diff --git a/app/build.gradle b/app/build.gradle index 4b70ac86..f9a12a36 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "org.qp.android" minSdk 26 targetSdk 34 - versionCode 202502 - versionName "3.25.2" + versionCode 202503 + versionName "3.25.3" resourceConfigurations += ['en', 'ru'] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { diff --git a/app/src/main/java/org/qp/android/helpers/utils/DirUtil.java b/app/src/main/java/org/qp/android/helpers/utils/DirUtil.java index 632ea0f4..e1c3671f 100644 --- a/app/src/main/java/org/qp/android/helpers/utils/DirUtil.java +++ b/app/src/main/java/org/qp/android/helpers/utils/DirUtil.java @@ -14,6 +14,7 @@ import com.anggrayudi.storage.file.DocumentFileCompat; import com.anggrayudi.storage.file.DocumentFileType; import com.anggrayudi.storage.file.DocumentFileUtils; +import com.anggrayudi.storage.file.MimeType; import java.util.Collections; import java.util.List; @@ -28,12 +29,27 @@ public static boolean isDirContainsGameFile(@NonNull Context context, @NonNull Uri dirUri) { var targetDir = DocumentFileCompat.fromUri(context, dirUri); if (!isWritableDir(context, targetDir)) return false; - var files = DocumentFileUtils.search(targetDir, true, DocumentFileType.FILE); + var files = targetDir.listFiles(); + if (files == null || files.length == 0) return false; + for (var file : files) { var dirExtension = documentWrap(file).getExtension(); var lcName = dirExtension.toLowerCase(Locale.ROOT); if (lcName.contains("qsp") || lcName.contains("gam")) return true; } + + var allFiles = DocumentFileUtils.search( + targetDir, + true, + DocumentFileType.FILE, + new String[]{MimeType.BINARY_FILE} + ); + for (var file : allFiles) { + var dirExtension = documentWrap(file).getExtension(); + var lcName = dirExtension.toLowerCase(Locale.ROOT); + if (lcName.contains("qsp") || lcName.contains("gam")) return true; + } + return false; } diff --git a/app/src/main/java/org/qp/android/model/repository/LocalGame.java b/app/src/main/java/org/qp/android/model/repository/LocalGame.java index 630e7f46..d0693534 100644 --- a/app/src/main/java/org/qp/android/model/repository/LocalGame.java +++ b/app/src/main/java/org/qp/android/model/repository/LocalGame.java @@ -32,6 +32,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -136,10 +137,11 @@ public boolean searchAndWriteFileInfo(DocumentFile rootDir, GameData data) { return false; } - var gameFiles = Collections.synchronizedList(new ArrayList()); - var files = DocumentFileUtils.search(rootDir, true, DocumentFileType.FILE); + var files = rootDir.listFiles(); + if (files == null || files.length == 0) return false; - files.parallelStream().forEach(d -> { + var gameFiles = Collections.synchronizedList(new ArrayList()); + Arrays.stream(files).forEach(d -> { var dirExtension = documentWrap(d).getExtension(); var lcName = dirExtension.toLowerCase(Locale.ROOT); if (lcName.endsWith("qsp") || lcName.endsWith("gam")) { @@ -147,6 +149,22 @@ public boolean searchAndWriteFileInfo(DocumentFile rootDir, GameData data) { } }); + if (gameFiles.isEmpty()) { + var allFiles = DocumentFileUtils.search( + rootDir, + true, + DocumentFileType.FILE, + new String[]{MimeType.BINARY_FILE} + ); + allFiles.forEach(d -> { + var dirExtension = documentWrap(d).getExtension(); + var lcName = dirExtension.toLowerCase(Locale.ROOT); + if (lcName.endsWith("qsp") || lcName.endsWith("gam")) { + gameFiles.add(d.getUri()); + } + }); + } + if (!Objects.equals(data.gameDirUri.getPath(), rootDir.getUri().getPath())) { data.gameDirUri = rootDir.getUri(); } @@ -166,7 +184,6 @@ public List lightExtractDataFromDir(File generalGamesDir) throws IOExc return Collections.emptyList(); } - var itemsGamesDirs = Collections.synchronizedList(new ArrayList()); var subRootDir = Collections.synchronizedList(new ArrayList()); synchronized (subRootDir) { try (var walk = Files.walk(generalGamesDir.toPath(), 1)) { @@ -178,26 +195,37 @@ public List lightExtractDataFromDir(File generalGamesDir) throws IOExc } } - synchronized (itemsGamesDirs) { + var subInfosFiles = Collections.synchronizedList(new ArrayList()); + synchronized (subInfosFiles) { for (var dir : subRootDir) { - if (!isWritableDir(context, dir)) { + try (var walk = Files.walk(dir.toPath())) { + walk.map(Path::toFile) + .filter(f -> f.isFile() && f.getPath().contains(GAME_INFO_FILENAME)) + .forEach(subInfosFiles::add); + } catch (IOException e) { + return Collections.emptyList(); + } + } + } + + var itemsGamesDirs = Collections.synchronizedList(new ArrayList()); + synchronized (itemsGamesDirs) { + for (var infos : subInfosFiles) { + if (!isWritableFile(context, infos)) { continue; } var item = (GameData) null; - var infoFile = fromRelPath(GAME_INFO_FILENAME, dir); - if (isWritableFile(context, infoFile)) { - var infoFileCont = readFileAsString(infoFile); - if (isNotEmptyOrBlank(infoFileCont)) { - try { - item = parseGameInfo(infoFileCont); - } catch (IOException e) { - continue; - } - } - if (item != null) { - itemsGamesDirs.add(item); + var infoFileCont = readFileAsString(infos); + if (isNotEmptyOrBlank(infoFileCont)) { + try { + item = parseGameInfo(infoFileCont); + } catch (IOException e) { + continue; } } + if (item != null) { + itemsGamesDirs.add(item); + } } } diff --git a/app/src/main/java/org/qp/android/ui/stock/StockActivity.java b/app/src/main/java/org/qp/android/ui/stock/StockActivity.java index b79cb49c..058a8f2e 100644 --- a/app/src/main/java/org/qp/android/ui/stock/StockActivity.java +++ b/app/src/main/java/org/qp/android/ui/stock/StockActivity.java @@ -4,6 +4,7 @@ import static org.qp.android.helpers.utils.FileUtil.findOrCreateFile; import static org.qp.android.helpers.utils.JsonUtil.jsonToObject; import static org.qp.android.helpers.utils.JsonUtil.objectToJson; +import static org.qp.android.helpers.utils.StringUtil.isNotEmptyOrBlank; import static org.qp.android.helpers.utils.XmlUtil.objectToXml; import static org.qp.android.helpers.utils.XmlUtil.xmlToObject; import static org.qp.android.ui.stock.StockViewModel.CODE_PICK_IMAGE_FILE; @@ -80,7 +81,6 @@ import java.util.Locale; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import info.hannes.changelog.ChangeLog; import okhttp3.ResponseBody; @@ -578,11 +578,11 @@ public boolean onQueryTextSubmit(String query) { @Override public boolean onQueryTextChange(String newText) { - var gameDataList = stockViewModel.getSortedGames(); + var gameDataList = stockViewModel.getGamesMap().values(); stockViewModel.setDataList( gameDataList.stream() - .filter(d -> d.title.toLowerCase().contains(newText.toLowerCase())) - .collect(Collectors.toList()) + .filter(r -> isNotEmptyOrBlank(r.title) && r.title.toLowerCase().contains(newText.toLowerCase())) + .toList() ); return true; } diff --git a/app/src/main/java/org/qp/android/ui/stock/StockViewModel.java b/app/src/main/java/org/qp/android/ui/stock/StockViewModel.java index f2ab0417..5e2500e6 100644 --- a/app/src/main/java/org/qp/android/ui/stock/StockViewModel.java +++ b/app/src/main/java/org/qp/android/ui/stock/StockViewModel.java @@ -180,16 +180,6 @@ private SettingsController getController() { return SettingsController.newInstance(getApplication()); } - @NonNull - public List getSortedGames() { - var unsortedGameData = gamesMap.values(); - var gameData = new ArrayList<>(unsortedGameData); - if (gameData.size() < 2) return gameData; - gameData.sort(Comparator.comparing(game -> game.title.toLowerCase())); - gameData.sort(Comparator.comparing(game -> game.listId)); - return gameData; - } - public Optional getCurrGameData() { return Optional.ofNullable(currGameData); } @@ -475,7 +465,7 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - tempList.addAll(getSortedGames()); + tempList.addAll(gamesMap.values()); isEnableDeleteMode = true; return true; } @@ -838,6 +828,7 @@ private void syncFromDisk() { if (syncDataList.size() < 2) return syncDataList; synchronized (syncDataList) { return syncDataList.stream() + .filter(game -> isNotEmptyOrBlank(game.title)) .sorted(Comparator.comparing(game -> game.title.toLowerCase())) .sorted(Comparator.comparing(game -> game.listId)) .filter(this::isGameInstalled) @@ -871,6 +862,7 @@ private void syncRemote() { if (syncDataList.size() < 2) return syncDataList; synchronized (syncDataList) { return syncDataList.stream() + .filter(game -> isNotEmptyOrBlank(game.title)) .sorted(Comparator.comparing(game -> game.title.toLowerCase())) .filter(d -> !isGameInstalled(d)) .toList();