From 4f7a2161914e6b5743164ca40794fb8a835703f9 Mon Sep 17 00:00:00 2001 From: Bohdan Shulyar Date: Wed, 26 Feb 2025 14:10:19 +0200 Subject: [PATCH] android: update codebase to use java File API --- .gitignore | 10 +- android/app/CMakeLists.txt | 40 ++++---- android/app/build.gradle.kts | 12 +-- .../java/su/xash/engine/DedicatedActivity.kt | 3 +- .../java/su/xash/engine/MainApplication.kt | 19 ++-- .../java/su/xash/engine/XashActivity.java | 19 +++- .../su/xash/engine/adapters/GameAdapter.kt | 9 +- .../su/xash/engine/model/BackgroundBitmap.kt | 33 ++++--- .../main/java/su/xash/engine/model/Game.kt | 49 +++++----- .../java/su/xash/engine/model/ModDatabase.kt | 59 ----------- .../xash/engine/ui/library/LibraryFragment.kt | 57 +++++++---- .../engine/ui/library/LibraryViewModel.kt | 82 ++-------------- .../engine/ui/settings/AppSettingsFragment.kt | 2 +- .../ui/settings/GameSettingsFragment.kt | 9 +- .../su/xash/engine/ui/setup/SetupFragment.kt | 48 --------- .../su/xash/engine/ui/setup/SetupViewModel.kt | 25 ----- .../ui/setup/pages/LocationPageFragment.kt | 62 ------------ .../ui/setup/pages/WelcomePageFragment.kt | 35 ------- .../{ => util}/AndroidBug5497Workaround.java | 2 +- .../su/xash/engine/workers/FileCopyWorker.kt | 98 ------------------- .../app/src/main/res/layout/activity_main.xml | 4 +- android/app/src/main/res/layout/card_game.xml | 13 --- .../main/res/layout/edit_text_preference.xml | 61 +++++++----- .../main/res/layout/fragment_app_settings.xml | 3 +- .../res/layout/fragment_game_settings.xml | 31 +++--- .../src/main/res/layout/fragment_library.xml | 6 +- .../src/main/res/layout/fragment_setup.xml | 10 -- .../src/main/res/layout/list_preference.xml | 8 +- .../app/src/main/res/layout/page_location.xml | 50 ---------- .../app/src/main/res/layout/page_welcome.xml | 50 ---------- .../src/main/res/layout/switch_preference.xml | 48 +++++---- .../src/main/res/menu/menu_game_settings.xml | 7 +- .../app/src/main/res/menu/menu_library.xml | 13 +-- .../app/src/main/res/navigation/nav_graph.xml | 11 --- .../src/main/res/values-pt-rBR/strings.xml | 12 --- .../app/src/main/res/values-ru/strings.xml | 12 --- android/app/src/main/res/values/strings.xml | 15 +-- 37 files changed, 247 insertions(+), 780 deletions(-) delete mode 100644 android/app/src/main/java/su/xash/engine/model/ModDatabase.kt delete mode 100644 android/app/src/main/java/su/xash/engine/ui/setup/SetupFragment.kt delete mode 100644 android/app/src/main/java/su/xash/engine/ui/setup/SetupViewModel.kt delete mode 100644 android/app/src/main/java/su/xash/engine/ui/setup/pages/LocationPageFragment.kt delete mode 100644 android/app/src/main/java/su/xash/engine/ui/setup/pages/WelcomePageFragment.kt rename android/app/src/main/java/su/xash/engine/{ => util}/AndroidBug5497Workaround.java (98%) delete mode 100644 android/app/src/main/java/su/xash/engine/workers/FileCopyWorker.kt delete mode 100644 android/app/src/main/res/layout/fragment_setup.xml delete mode 100644 android/app/src/main/res/layout/page_location.xml delete mode 100644 android/app/src/main/res/layout/page_welcome.xml diff --git a/.gitignore b/.gitignore index ee36f6c2..e44143d9 100644 --- a/.gitignore +++ b/.gitignore @@ -222,7 +222,7 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj @@ -345,3 +345,11 @@ enc_temp_folder/ # ccls langauge server .ccls-* + +# JetBrains +.idea/ +cmake-build-* + +# some Android-specific build stuff +3rdparty/SDL +3rdparty/hlsdk-portable diff --git a/android/app/CMakeLists.txt b/android/app/CMakeLists.txt index 77f01d81..54596788 100644 --- a/android/app/CMakeLists.txt +++ b/android/app/CMakeLists.txt @@ -1,29 +1,29 @@ -cmake_minimum_required(VERSION 3.22) +cmake_minimum_required(VERSION 3.22.1) # Only used to build Android project -project(XASH_ANDROID) +project(xash3d-fwgs-android) # armeabi-v7a requires cpufeatures library -if(ANDROID) - include_directories(${ANDROID_NDK}/sources/android/cpufeatures) - add_library(cpufeatures ${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c) - target_link_libraries(cpufeatures dl) -endif() +if (ANDROID) + include_directories(${ANDROID_NDK}/sources/android/cpufeatures) + add_library(cpufeatures ${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c) + target_link_libraries(cpufeatures dl) +endif () include(FindPython) if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") - set(BUILD_TYPE "debug") + set(BUILD_TYPE "debug") else() - set(BUILD_TYPE "release") - list(APPEND WAF_EXTRA_ARGS --enable-poly-opt --enable-lto --enable-limited-debuginfo) + set(BUILD_TYPE "release") + list(APPEND WAF_EXTRA_ARGS --enable-poly-opt --enable-lto --enable-limited-debuginfo) endif() -if(ANDROID_ABI STREQUAL "x86") - # HACKHACK: I don't know why but engine gets built as 64-bit binary here - list(APPEND WAF_EXTRA_ARGS -4) -endif() +if (ANDROID_ABI STREQUAL "x86") + # HACKHACK: I don't know why but engine gets built as 64-bit binary here + list(APPEND WAF_EXTRA_ARGS -4) +endif () set(CMAKE_VERBOSE_MAKEFILE ON) @@ -36,15 +36,9 @@ set(WAF_CXX "${CMAKE_CXX_COMPILER} --target=${CMAKE_CXX_COMPILER_TARGET}") set(WAF ${Python_EXECUTABLE} ${ENGINE_SOURCE_DIR}waf -t ${ENGINE_SOURCE_DIR} -o ${CMAKE_CURRENT_BINARY_DIR}/xash3d-fwgs) # try to build minimal SDL. Enable features as we're gonna use them -set(SDL_RENDER OFF) -set(SDL_POWER OFF) -set(SDL_VULKAN OFF) -set(SDL_DISKAUDIO OFF) -set(SDL_DUMMYAUDIO OFF) -set(SDL_DUMMYVIDEO OFF) -set(SDL_VULKAN OFF) -set(SDL_OFFSCREEN OFF) -set(SDL_STATIC OFF) +foreach (X IN ITEMS SDL_RENDER SDL_POWER SDL_VULKAN SDL_DISKAUDIO SDL_DUMMYAUDIO SDL_DUMMYVIDEO SDL_VULKAN SDL_OFFSCREEN SDL_STATIC) + set(${X} OFF CACHE BOOL "" FORCE) +endforeach () add_subdirectory("${ENGINE_SOURCE_DIR}/3rdparty/SDL" SDL) include(ExternalProject) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 0e96e9af..412d69e7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -49,6 +49,12 @@ android { noCompress += "" } + packaging { + jniLibs { + keepDebugSymbols.add("**/*.so") + } + } + sourceSets { getByName("main") { assets.srcDirs("../../3rdparty/extras/xash-extras") @@ -56,12 +62,6 @@ android { } } - packaging { - jniLibs { - keepDebugSymbols.add("**/*.so") - } - } - buildTypes { debug { isMinifyEnabled = false diff --git a/android/app/src/main/java/su/xash/engine/DedicatedActivity.kt b/android/app/src/main/java/su/xash/engine/DedicatedActivity.kt index 81d1cd89..0c8d0e8c 100644 --- a/android/app/src/main/java/su/xash/engine/DedicatedActivity.kt +++ b/android/app/src/main/java/su/xash/engine/DedicatedActivity.kt @@ -1,4 +1,3 @@ package su.xash.engine -class DedicatedActivity { -} \ No newline at end of file +class DedicatedActivity {} diff --git a/android/app/src/main/java/su/xash/engine/MainApplication.kt b/android/app/src/main/java/su/xash/engine/MainApplication.kt index 3ecb28fe..795271d6 100644 --- a/android/app/src/main/java/su/xash/engine/MainApplication.kt +++ b/android/app/src/main/java/su/xash/engine/MainApplication.kt @@ -3,23 +3,20 @@ package su.xash.engine import android.app.Application import android.content.Context import android.os.StrictMode -import org.acra.config.httpSender -import org.acra.data.StringFormat -import org.acra.ktx.initAcra class MainApplication : Application() { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) if (!BuildConfig.DEBUG) { - initAcra { - buildConfigClass = BuildConfig::class.java - reportFormat = StringFormat.JSON - - httpSender { - uri = "http://bodis.pp.ua:5000/report" - } - } +// initAcra { +// buildConfigClass = BuildConfig::class.java +// reportFormat = StringFormat.JSON +// +// httpSender { +// uri = "http://bodis.pp.ua:5000/report" +// } +// } } else { // enable strict mode to detect memory leaks etc. StrictMode.enableDefaults(); diff --git a/android/app/src/main/java/su/xash/engine/XashActivity.java b/android/app/src/main/java/su/xash/engine/XashActivity.java index f8592630..ff4cf61e 100644 --- a/android/app/src/main/java/su/xash/engine/XashActivity.java +++ b/android/app/src/main/java/su/xash/engine/XashActivity.java @@ -1,10 +1,13 @@ package su.xash.engine; import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.AssetManager; import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.provider.Settings.Secure; import android.util.Log; import android.view.KeyEvent; @@ -12,6 +15,8 @@ import android.view.WindowManager; import org.libsdl.app.SDLActivity; +import su.xash.engine.util.AndroidBug5497Workaround; + public class XashActivity extends SDLActivity { private boolean mUseVolumeKeys; private String mPackageName; @@ -106,11 +111,7 @@ public class XashActivity extends SDLActivity { int keyCode = event.getKeyCode(); if (!mUseVolumeKeys) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || - keyCode == KeyEvent.KEYCODE_VOLUME_UP || - keyCode == KeyEvent.KEYCODE_CAMERA || - keyCode == KeyEvent.KEYCODE_ZOOM_IN || - keyCode == KeyEvent.KEYCODE_ZOOM_OUT) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_CAMERA || keyCode == KeyEvent.KEYCODE_ZOOM_IN || keyCode == KeyEvent.KEYCODE_ZOOM_OUT) { return false; } } @@ -131,6 +132,14 @@ public class XashActivity extends SDLActivity { String pakfile = getIntent().getStringExtra("pakfile"); if (pakfile != null) nativeSetenv("XASH3D_EXTRAS_PAK2", pakfile); + String basedir = getIntent().getStringExtra("basedir"); + if (basedir != null) { + nativeSetenv("XASH3D_BASEDIR", basedir); + } else { + String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/xash"; + nativeSetenv("XASH3D_BASEDIR", rootPath); + } + mUseVolumeKeys = getIntent().getBooleanExtra("usevolume", false); mPackageName = getIntent().getStringExtra("package"); diff --git a/android/app/src/main/java/su/xash/engine/adapters/GameAdapter.kt b/android/app/src/main/java/su/xash/engine/adapters/GameAdapter.kt index d89e87bb..7e5ab4ef 100644 --- a/android/app/src/main/java/su/xash/engine/adapters/GameAdapter.kt +++ b/android/app/src/main/java/su/xash/engine/adapters/GameAdapter.kt @@ -31,7 +31,7 @@ class GameAdapter(private val libraryViewModel: LibraryViewModel) : } override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { - return oldItem.basedir.name == newItem.basedir.name && oldItem.installed == newItem.installed + return oldItem.basedir.name == newItem.basedir.name } } @@ -54,13 +54,6 @@ class GameAdapter(private val libraryViewModel: LibraryViewModel) : gameCover.visibility = View.GONE } - if (!game.installed) { - launchButton.visibility = View.GONE - settingsButton.visibility = View.GONE - progressIndicator.visibility = View.VISIBLE - return - } - settingsButton.setOnClickListener { libraryViewModel.setSelectedGame(game) it.findNavController() diff --git a/android/app/src/main/java/su/xash/engine/model/BackgroundBitmap.kt b/android/app/src/main/java/su/xash/engine/model/BackgroundBitmap.kt index d2726974..83e7d3ff 100644 --- a/android/app/src/main/java/su/xash/engine/model/BackgroundBitmap.kt +++ b/android/app/src/main/java/su/xash/engine/model/BackgroundBitmap.kt @@ -1,10 +1,10 @@ package su.xash.engine.model -import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas -import androidx.documentfile.provider.DocumentFile import su.xash.engine.util.TGAReader +import java.io.File +import java.io.FileInputStream import java.util.Scanner @@ -14,7 +14,7 @@ object BackgroundBitmap { private const val BACKGROUND_WIDTH = 800 private const val BACKGROUND_HEIGHT = 600 - fun createBackground(ctx: Context, file: DocumentFile): Bitmap { + fun createBackground(file: File): Bitmap { var bitmap = Bitmap.createBitmap(BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888) var canvas = Canvas(bitmap) @@ -23,19 +23,20 @@ object BackgroundBitmap { var width: Int var height = 0 - var bgLayout = file.findFile("resource")?.findFile("HD_BackgroundLayout.txt") - if (bgLayout == null) { - bgLayout = file.findFile("resource")?.findFile("BackgroundLayout.txt") + val resourceFolder = File(file, "resource") + var bgLayout = File(resourceFolder, "HD_BackgroundLayout.txt") + if (!bgLayout.exists()) { + bgLayout = File(resourceFolder, "BackgroundLayout.txt") } - if (bgLayout == null) { - val dir = file.findFile("resource")?.findFile("background") + if (!bgLayout.exists()) { + val dir = File(resourceFolder, "background") for (i in 0 until BACKGROUND_ROWS) { x = 0 for (j in 0 until BACKGROUND_COLUMNS) { val filename = "${BACKGROUND_WIDTH}_${i + 1}_${'a' + j}_loading.tga" - val bmpFile = dir?.findFile(filename) - val bmpImage = loadTga(ctx, bmpFile!!) + val bmpFile = File(dir, filename) + val bmpImage = loadTga(bmpFile) canvas.drawBitmap(bmpImage, x.toFloat(), y.toFloat(), null) x += bmpImage.width @@ -47,7 +48,7 @@ object BackgroundBitmap { return bitmap } - ctx.contentResolver.openInputStream(bgLayout.uri).use { inputStream -> + FileInputStream(bgLayout).use { inputStream -> Scanner(inputStream).use { scanner -> while (scanner.hasNext()) { when (val str = scanner.next()) { @@ -60,12 +61,12 @@ object BackgroundBitmap { else -> { var bmpFile = file - str.split("/").forEach { bmpFile = bmpFile.findFile(it)!! } + str.split("/").forEach { bmpFile = File(bmpFile, it) } //skip scanner.next() x = scanner.nextInt() y = scanner.nextInt() - val bmp = loadTga(ctx, bmpFile) + val bmp = loadTga(bmpFile) canvas.drawBitmap(bmp, x.toFloat(), y.toFloat(), null) } } @@ -75,9 +76,9 @@ object BackgroundBitmap { return bitmap } - private fun loadTga(ctx: Context, file: DocumentFile): Bitmap { - ctx.contentResolver.openInputStream(file.uri).use { - val buffer = it?.readBytes() + private fun loadTga(file: File): Bitmap { + FileInputStream(file).use { + val buffer = it.readBytes() val pixels = TGAReader.read(buffer, TGAReader.ARGB) val width = TGAReader.getWidth(buffer) diff --git a/android/app/src/main/java/su/xash/engine/model/Game.kt b/android/app/src/main/java/su/xash/engine/model/Game.kt index dc4b307e..5ea6fab4 100644 --- a/android/app/src/main/java/su/xash/engine/model/Game.kt +++ b/android/app/src/main/java/su/xash/engine/model/Game.kt @@ -5,13 +5,14 @@ import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.net.Uri -import android.provider.MediaStore -import androidx.documentfile.provider.DocumentFile import su.xash.engine.XashActivity +import java.io.File +import java.io.FileInputStream -class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = true) { +class Game(val ctx: Context, val basedir: File) { private var iconName = "game.ico" var title = "Unknown Game" var icon: Bitmap? = null @@ -20,15 +21,21 @@ class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = private val pref = ctx.getSharedPreferences(basedir.name, Context.MODE_PRIVATE) init { - basedir.findFile("gameinfo.txt")?.let { - parseGameInfo(it) - } ?: basedir.findFile("liblist.gam")?.let { parseGameInfo(it) } + val gameInfo = File(basedir, "gameinfo.txt") + if (gameInfo.exists()) { + parseGameInfo(gameInfo) + } else { + val libListGam = File(basedir, "liblist.gam") + if (libListGam.exists()) parseGameInfo(libListGam) + } - basedir.findFile(iconName) - ?.let { icon = MediaStore.Images.Media.getBitmap(ctx.contentResolver, it.uri) } + val iconFile = File(basedir, iconName) + if (iconFile.exists()) { + icon = BitmapFactory.decodeFile(iconFile.path) + } try { - cover = BackgroundBitmap.createBackground(ctx, basedir) + cover = BackgroundBitmap.createBackground(basedir) } catch (e: Exception) { e.printStackTrace() } @@ -40,15 +47,16 @@ class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = putExtra("gamedir", basedir.name) putExtra("argv", pref.getString("arguments", "-console -log")) putExtra("usevolume", pref.getBoolean("use_volume_buttons", false)) + putExtra("basedir", basedir.parent) //.putExtra("gamelibdir", getGameLibDir(context)) //.putExtra("package", getPackageName()) } }) } - private fun parseGameInfo(file: DocumentFile) { - ctx.contentResolver.openInputStream(file.uri).use { inputStream -> - inputStream?.bufferedReader().use { reader -> - reader?.forEachLine { + private fun parseGameInfo(file: File) { + FileInputStream(file).use { inputStream -> + inputStream.bufferedReader().use { reader -> + reader.forEachLine { val tokens = it.split("\\s+".toRegex(), limit = 2) if (tokens.size >= 2) { val k = tokens[0] @@ -83,19 +91,19 @@ class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = ) return null } - return pkgInfo.applicationInfo.nativeLibraryDir + return pkgInfo.applicationInfo?.nativeLibraryDir } return ctx.applicationInfo.nativeLibraryDir } companion object { - fun getGames(ctx: Context, file: DocumentFile): List { + fun getGames(ctx: Context, file: File): List { val games = mutableListOf() if (checkIfGamedir(file)) { games.add(Game(ctx, file)) } else { - file.listFiles().forEach { + file.listFiles()?.forEach { if (it.isDirectory) { if (checkIfGamedir(it)) { games.add(Game(ctx, it)) @@ -107,13 +115,10 @@ class Game(val ctx: Context, val basedir: DocumentFile, var installed: Boolean = return games } - fun checkIfGamedir(file: DocumentFile): Boolean { - // exclude unfinished downloads - if (file.name?.startsWith('.') == true) - return false + fun checkIfGamedir(file: File): Boolean { + if (File(file, "liblist.gam").exists()) return true + if (File(file, "gameinfo.txt").exists()) return true - file.findFile("liblist.gam")?.let { return true } - file.findFile("gameinfo.txt")?.let { return true } return false } } diff --git a/android/app/src/main/java/su/xash/engine/model/ModDatabase.kt b/android/app/src/main/java/su/xash/engine/model/ModDatabase.kt deleted file mode 100644 index 8fed465e..00000000 --- a/android/app/src/main/java/su/xash/engine/model/ModDatabase.kt +++ /dev/null @@ -1,59 +0,0 @@ -package su.xash.engine.model - -import org.json.JSONArray -import org.json.JSONObject -import java.io.InputStream - -class ModDatabase(inputStream: InputStream) { - val entries = mutableListOf() - - companion object { - const val VERSION = 1 - - fun getFilename(): String { - return "v${VERSION}.json" - } - } - - init { - inputStream.bufferedReader().use { - val jsonArray = JSONArray(it.readText()) - - for (i in 0..= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(R.string.file_access_required) + setMessage(R.string.file_access_message) + setPositiveButton(android.R.string.ok) { _, _ -> + startActivityForResult.launch( + Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).setData( + Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) + ) + ) + } + setCancelable(false) + show() + } + + return false + } else { + return true + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = FragmentLibraryBinding.inflate(inflater, container, false) val adapter = GameAdapter(libraryViewModel) @@ -47,11 +84,7 @@ class LibraryFragment : Fragment(), MenuProvider { (binding.gamesList.adapter as GameAdapter).submitList(it) } - libraryViewModel.workInfos.observe(viewLifecycleOwner) { - libraryViewModel.refreshDownloads(requireContext()) - } - - libraryViewModel.downloads.observe(viewLifecycleOwner) { + if (checkStoragePermissions()) { libraryViewModel.reloadGames(requireContext()) } } @@ -67,18 +100,6 @@ class LibraryFragment : Fragment(), MenuProvider { override fun onMenuItemSelected(menuItem: MenuItem): Boolean { when (menuItem.itemId) { - R.id.action_browse -> { - startActivity( - Intent(Intent.ACTION_VIEW).setDataAndType( - null, "vnd.android.document/directory" - ) - ) - } - - R.id.action_install -> { - findNavController().navigate(R.id.action_libraryFragment_to_setupFragment) - } - R.id.action_settings -> { findNavController().navigate(R.id.action_libraryFragment_to_appSettingsFragment) } diff --git a/android/app/src/main/java/su/xash/engine/ui/library/LibraryViewModel.kt b/android/app/src/main/java/su/xash/engine/ui/library/LibraryViewModel.kt index 90af9d0c..91a20b4f 100644 --- a/android/app/src/main/java/su/xash/engine/ui/library/LibraryViewModel.kt +++ b/android/app/src/main/java/su/xash/engine/ui/library/LibraryViewModel.kt @@ -2,49 +2,30 @@ package su.xash.engine.ui.library import android.app.Application import android.content.Context -import android.net.Uri -import androidx.documentfile.provider.DocumentFile +import android.content.SharedPreferences +import android.os.Environment import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import androidx.work.Data -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkInfo -import androidx.work.WorkManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import su.xash.engine.model.Game -import su.xash.engine.model.ModDatabase -import su.xash.engine.workers.FileCopyWorker -import su.xash.engine.workers.KEY_FILE_URI - -const val TAG_INSTALL = "TAG_INSTALL" +import java.io.File class LibraryViewModel(application: Application) : AndroidViewModel(application) { val installedGames: LiveData> get() = _installedGames private val _installedGames = MutableLiveData(emptyList()) - val downloads: LiveData> get() = _downloads - private val _downloads = MutableLiveData(emptyList()) - val isReloading: LiveData get() = _isReloading private val _isReloading = MutableLiveData(false) - private val workManager = WorkManager.getInstance(application.applicationContext) - val workInfos: LiveData> = workManager.getWorkInfosByTagLiveData(TAG_INSTALL) - val selectedItem: LiveData get() = _selectedItem private val _selectedItem = MutableLiveData() - val appPreferences = application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) - val modDb: ModDatabase - - init { - modDb = application.assets.open(ModDatabase.getFilename()).use { ModDatabase(it) } - reloadGames(application.applicationContext) - } + private val appPreferences: SharedPreferences = + application.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) fun reloadGames(ctx: Context) { if (isReloading.value == true) { @@ -54,65 +35,20 @@ class LibraryViewModel(application: Application) : AndroidViewModel(application) viewModelScope.launch { withContext(Dispatchers.IO) { - val games = mutableListOf() - val root = DocumentFile.fromFile(ctx.getExternalFilesDir(null)!!) + val rootPath = appPreferences.getString("game_path", null) + ?: (Environment.getExternalStorageDirectory().absolutePath + "/xash") + val root = File(rootPath) - val installedGames = Game.getGames(ctx, root) - .filter { p -> _downloads.value?.any { p.basedir.name == it.basedir.name } == false } - - games.addAll(installedGames) - downloads.value?.let { games.addAll(it) } - - _installedGames.postValue(games) + _installedGames.postValue(Game.getGames(ctx, root)) _isReloading.postValue(false) } } } - fun refreshDownloads(ctx: Context) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - val games = mutableListOf() - - workInfos.value?.filter { - it.state == WorkInfo.State.RUNNING && !it.progress.getString(FileCopyWorker.Input) - .isNullOrEmpty() - }?.forEach { - val uri = Uri.parse(it.progress.getString(FileCopyWorker.Input)) - val file = DocumentFile.fromTreeUri(ctx, uri) - games.addAll(Game.getGames(ctx, file!!)) - games.forEach { g -> g.installed = false } - } - - _downloads.postValue(games) - } - } - } - - fun installGame(uri: Uri) { - val data = Data.Builder().putString(KEY_FILE_URI, uri.toString()).build() - val request = OneTimeWorkRequestBuilder().run { - setInputData(data) - addTag(TAG_INSTALL) - build() - } - workManager.enqueue(request) - } - fun setSelectedGame(game: Game) { _selectedItem.value = game } - fun uninstallGame(game: Game) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - game.installed = false - game.basedir.delete() - _installedGames.postValue(_installedGames.value) - } - } - } - fun startEngine(ctx: Context, game: Game) { game.startEngine(ctx) } diff --git a/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsFragment.kt b/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsFragment.kt index 7bdb08a8..e3bef9fd 100644 --- a/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsFragment.kt +++ b/android/app/src/main/java/su/xash/engine/ui/settings/AppSettingsFragment.kt @@ -13,7 +13,7 @@ class AppSettingsFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = FragmentAppSettingsBinding.inflate(inflater, container, false) return binding.root } diff --git a/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsFragment.kt b/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsFragment.kt index dd7bd2fb..d9fbc30e 100644 --- a/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsFragment.kt +++ b/android/app/src/main/java/su/xash/engine/ui/settings/GameSettingsFragment.kt @@ -6,8 +6,6 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import su.xash.engine.R import su.xash.engine.databinding.FragmentGameSettingsBinding import su.xash.engine.ui.library.LibraryViewModel @@ -19,7 +17,7 @@ class GameSettingsFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = FragmentGameSettingsBinding.inflate(inflater, container, false) return binding.root } @@ -50,11 +48,6 @@ class GameSettingsFragment : Fragment() { childFragmentManager.beginTransaction() .add(binding.settingsFragment.id, GameSettingsPreferenceFragment(game)) .commit(); - - binding.bottomNavigation.menu.findItem(R.id.action_uninstall).setOnMenuItemClickListener { - libraryViewModel.uninstallGame(game) - findNavController().popBackStack() - } } override fun onDestroyView() { diff --git a/android/app/src/main/java/su/xash/engine/ui/setup/SetupFragment.kt b/android/app/src/main/java/su/xash/engine/ui/setup/SetupFragment.kt deleted file mode 100644 index e15f9dc0..00000000 --- a/android/app/src/main/java/su/xash/engine/ui/setup/SetupFragment.kt +++ /dev/null @@ -1,48 +0,0 @@ -package su.xash.engine.ui.setup - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.viewpager2.adapter.FragmentStateAdapter -import su.xash.engine.databinding.FragmentSetupBinding -import su.xash.engine.ui.setup.pages.LocationPageFragment -import su.xash.engine.ui.setup.pages.WelcomePageFragment - -class SetupFragment : Fragment() { - private var _binding: FragmentSetupBinding? = null - private val binding get() = _binding!! - private val setupViewModel: SetupViewModel by activityViewModels() - - private lateinit var setupPageAdapter: SetupPageAdapter - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - _binding = FragmentSetupBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - setupPageAdapter = SetupPageAdapter(this) - binding.viewPager.isUserInputEnabled = false - binding.viewPager.adapter = setupPageAdapter - - setupViewModel.pageNumber.observe(viewLifecycleOwner) { - binding.viewPager.setCurrentItem(it, true) - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} - -class SetupPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { - val pages = listOf(WelcomePageFragment(), LocationPageFragment()) - override fun getItemCount(): Int = 2 - override fun createFragment(position: Int): Fragment = pages[position] -} diff --git a/android/app/src/main/java/su/xash/engine/ui/setup/SetupViewModel.kt b/android/app/src/main/java/su/xash/engine/ui/setup/SetupViewModel.kt deleted file mode 100644 index 94779cc0..00000000 --- a/android/app/src/main/java/su/xash/engine/ui/setup/SetupViewModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package su.xash.engine.ui.setup - -import android.app.Application -import android.net.Uri -import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import su.xash.engine.MainApplication -import su.xash.engine.model.Game - -class SetupViewModel(application: Application) : AndroidViewModel(application) { - val pageNumber: LiveData get() = _pageNumber - private val _pageNumber = MutableLiveData(0) - - fun checkIfGameDir(uri: Uri): Boolean { - val ctx = getApplication().applicationContext - val file = DocumentFile.fromTreeUri(ctx, uri)!! - return Game.checkIfGamedir(file) - } - - fun setPageNumber(pos: Int) { - _pageNumber.value = pos - } -} diff --git a/android/app/src/main/java/su/xash/engine/ui/setup/pages/LocationPageFragment.kt b/android/app/src/main/java/su/xash/engine/ui/setup/pages/LocationPageFragment.kt deleted file mode 100644 index a5b1f72d..00000000 --- a/android/app/src/main/java/su/xash/engine/ui/setup/pages/LocationPageFragment.kt +++ /dev/null @@ -1,62 +0,0 @@ -package su.xash.engine.ui.setup.pages - -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import su.xash.engine.R -import su.xash.engine.databinding.PageLocationBinding -import su.xash.engine.ui.library.LibraryViewModel -import su.xash.engine.ui.setup.SetupViewModel - -class LocationPageFragment : Fragment() { - private var _binding: PageLocationBinding? = null - private val binding get() = _binding!! - private val setupViewModel: SetupViewModel by activityViewModels() - private val libraryViewModel: LibraryViewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - _binding = PageLocationBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.pageButton.setOnClickListener { - getGamesDirectory.launch(null) - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private val getGamesDirectory = - registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { - it?.let { - if (!setupViewModel.checkIfGameDir(it)) { - MaterialAlertDialogBuilder(requireContext()).apply { - setTitle(R.string.error) - setMessage(R.string.setup_location_empty) - setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } - show() - } - } else { - requireContext().contentResolver.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - libraryViewModel.installGame(it) - findNavController().navigate(R.id.action_setupFragment_to_libraryFragment) - } - } - } -} diff --git a/android/app/src/main/java/su/xash/engine/ui/setup/pages/WelcomePageFragment.kt b/android/app/src/main/java/su/xash/engine/ui/setup/pages/WelcomePageFragment.kt deleted file mode 100644 index 09327b6b..00000000 --- a/android/app/src/main/java/su/xash/engine/ui/setup/pages/WelcomePageFragment.kt +++ /dev/null @@ -1,35 +0,0 @@ -package su.xash.engine.ui.setup.pages - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import su.xash.engine.databinding.PageWelcomeBinding -import su.xash.engine.ui.setup.SetupViewModel - -class WelcomePageFragment : Fragment() { - private var _binding: PageWelcomeBinding? = null - private val binding get() = _binding!! - private val setupViewModel: SetupViewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - _binding = PageWelcomeBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.pageButton.setOnClickListener { - setupViewModel.setPageNumber(1) - } - setupViewModel.setPageNumber(0) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/android/app/src/main/java/su/xash/engine/AndroidBug5497Workaround.java b/android/app/src/main/java/su/xash/engine/util/AndroidBug5497Workaround.java similarity index 98% rename from android/app/src/main/java/su/xash/engine/AndroidBug5497Workaround.java rename to android/app/src/main/java/su/xash/engine/util/AndroidBug5497Workaround.java index 008a58ec..27826557 100644 --- a/android/app/src/main/java/su/xash/engine/AndroidBug5497Workaround.java +++ b/android/app/src/main/java/su/xash/engine/util/AndroidBug5497Workaround.java @@ -1,4 +1,4 @@ -package su.xash.engine; +package su.xash.engine.util; import android.app.Activity; import android.graphics.Rect; diff --git a/android/app/src/main/java/su/xash/engine/workers/FileCopyWorker.kt b/android/app/src/main/java/su/xash/engine/workers/FileCopyWorker.kt deleted file mode 100644 index ce7569e7..00000000 --- a/android/app/src/main/java/su/xash/engine/workers/FileCopyWorker.kt +++ /dev/null @@ -1,98 +0,0 @@ -package su.xash.engine.workers - -import android.content.Context -import android.net.Uri -import androidx.documentfile.provider.DocumentFile -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import androidx.work.workDataOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -const val KEY_FILE_URI = "KEY_FILE_URI" - -class FileCopyWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { - companion object { - const val Input = "Input" - const val Progress = "Progress" - } - - private var fileCount = 0 - private var fileCopied = 0 - - override suspend fun doWork(): Result { - withContext(Dispatchers.IO) { - val fileUri = inputData.getString(KEY_FILE_URI) - setProgress(workDataOf(Input to fileUri)) - - val uri = Uri.parse(fileUri) - val source = DocumentFile.fromTreeUri(applicationContext, uri) - - fileCount = source?.countDirFiles() ?: return@withContext Result.failure() - - setProgress(workDataOf(Progress to 0f)) - - val gamedir = source.name!! - val externalFilesDir = - DocumentFile.fromFile(applicationContext.getExternalFilesDir(null)!!) - - // create a directory to store staged files - val target = externalFilesDir.createDirectory(".$gamedir")!! - - source.copyDirTo(applicationContext, this@FileCopyWorker, target) - - target.renameTo(gamedir) - - setProgress(workDataOf(Progress to 1f)) - } - return Result.success() - } - - suspend fun fileCopied(count: Int) { - if (count == 0) - return - - fileCopied += count - val percentage: Float = fileCopied.toFloat() / fileCount.toFloat(); - setProgress(workDataOf(Progress to percentage)) - } -} - -fun DocumentFile.countDirFiles(): Int { - var count: Int = 0 - - listFiles().forEach { - if (it.isDirectory) - count += it.countDirFiles() - else - count++ - } - - return count -} - -fun DocumentFile.copyFileTo(ctx: Context, file: DocumentFile) { - val outFile = file.createFile("application", name!!)!! - - ctx.contentResolver.openOutputStream(outFile.uri).use { os -> - ctx.contentResolver.openInputStream(uri).use { - it?.copyTo(os!!) - } - } -} - -suspend fun DocumentFile.copyDirTo(ctx: Context, worker: FileCopyWorker, dir: DocumentFile) { - var count: Int = 0 - - listFiles().forEach { - if (it.isDirectory) { - val outDir = dir.createDirectory(it.name!!)!! - it.copyDirTo(ctx, worker, outDir) - } else { - it.copyFileTo(ctx, dir) - count++ - } - } - - worker.fileCopied(count) -} diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 338a67aa..5c95cf48 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -1,9 +1,9 @@ + android:layout_height="match_parent"> - - - + android:layout_height="wrap_content" + android:layout_marginHorizontal="8dp" + android:layout_marginTop="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent"> - - - - - - - - - - - + android:padding="10dp"> + + + + + + + + + + + + + + + - - diff --git a/android/app/src/main/res/layout/fragment_app_settings.xml b/android/app/src/main/res/layout/fragment_app_settings.xml index a9d905f2..73a953d8 100644 --- a/android/app/src/main/res/layout/fragment_app_settings.xml +++ b/android/app/src/main/res/layout/fragment_app_settings.xml @@ -2,8 +2,7 @@ + android:layout_height="match_parent"> - + app:layout_constraintTop_toBottomOf="@id/gameCard" + tools:layout="@android:layout/list_content"> - + - - - + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_library.xml b/android/app/src/main/res/layout/fragment_library.xml index 25b7cd57..12ef63d3 100644 --- a/android/app/src/main/res/layout/fragment_library.xml +++ b/android/app/src/main/res/layout/fragment_library.xml @@ -2,9 +2,9 @@ + android:layout_height="match_parent"> diff --git a/android/app/src/main/res/layout/fragment_setup.xml b/android/app/src/main/res/layout/fragment_setup.xml deleted file mode 100644 index 4ada2dc6..00000000 --- a/android/app/src/main/res/layout/fragment_setup.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/layout/list_preference.xml b/android/app/src/main/res/layout/list_preference.xml index 78eb2fd2..3348546d 100644 --- a/android/app/src/main/res/layout/list_preference.xml +++ b/android/app/src/main/res/layout/list_preference.xml @@ -8,13 +8,13 @@ android:id="@android:id/title" android:layout_width="match_parent" android:layout_height="wrap_content" - app:layout_constraintTop_toTopOf="parent" - android:textAppearance="@style/TextAppearance.Material3.TitleMedium" /> + android:textAppearance="@style/TextAppearance.Material3.TitleMedium" + app:layout_constraintTop_toTopOf="parent" /> + android:textAppearance="@style/TextAppearance.Material3.BodyMedium" + app:layout_constraintTop_toBottomOf="@android:id/title" /> diff --git a/android/app/src/main/res/layout/page_location.xml b/android/app/src/main/res/layout/page_location.xml deleted file mode 100644 index 8d6efb10..00000000 --- a/android/app/src/main/res/layout/page_location.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - -