Unified Import Source configuration, added concurrency prevention and combined UI
This commit is contained in:
@@ -10,6 +10,7 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
@@ -33,7 +34,7 @@ import com.majinnaibu.monstercards.importers.GitRepoImporterService;
|
||||
import com.majinnaibu.monstercards.importers.Open5eApiWrapper;
|
||||
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
||||
import com.majinnaibu.monstercards.models.BinderExport;
|
||||
import com.majinnaibu.monstercards.models.GitRepositorySource;
|
||||
import com.majinnaibu.monstercards.models.ImportSource;
|
||||
import com.majinnaibu.monstercards.models.Monster;
|
||||
import com.majinnaibu.monstercards.utils.Logger;
|
||||
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||
@@ -53,10 +54,12 @@ import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private Disposable mOpen5eImportDisposable;
|
||||
private Disposable mGitImportDisposable;
|
||||
private Disposable mImportDisposable;
|
||||
private ImportSource mCurrentImportSource;
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
@@ -212,83 +215,73 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
public void importAllFromOpen5e() {
|
||||
if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) {
|
||||
ToastHelper.showShort(this, "Open5e import already running");
|
||||
public void startImportFromSource(ImportSource source) {
|
||||
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.dialog_import_already_running_title)
|
||||
.setMessage(getString(R.string.dialog_import_already_running_message, mCurrentImportSource != null ? mCurrentImportSource.projectName : ""))
|
||||
.setPositiveButton(R.string.action_cancel_current, (dialog, which) -> {
|
||||
mImportDisposable.dispose();
|
||||
executeImportFromSource(source);
|
||||
})
|
||||
.setNegativeButton(R.string.dialog_cancel, null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
executeImportFromSource(source);
|
||||
}
|
||||
|
||||
private void executeImportFromSource(ImportSource source) {
|
||||
mCurrentImportSource = source;
|
||||
View rootView = findViewById(android.R.id.content);
|
||||
Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, R.string.snackbar_importing_open5e);
|
||||
Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, getString(R.string.snackbar_importing_source, source.projectName));
|
||||
snackbar.setAction(R.string.action_cancel, v -> {
|
||||
if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) {
|
||||
mOpen5eImportDisposable.dispose();
|
||||
SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_cancelled);
|
||||
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
|
||||
mImportDisposable.dispose();
|
||||
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_cancelled, source.projectName));
|
||||
}
|
||||
});
|
||||
snackbar.show();
|
||||
|
||||
mOpen5eImportDisposable = Single.fromCallable(() -> {
|
||||
int totalImported = 0;
|
||||
String nextUrl = null;
|
||||
do {
|
||||
if (mOpen5eImportDisposable != null && mOpen5eImportDisposable.isDisposed()) {
|
||||
break;
|
||||
}
|
||||
Open5eApiWrapper.Open5ePageResult page = Open5eApiWrapper.fetchPage(nextUrl);
|
||||
for (Monster monster : page.monsters) {
|
||||
if (mOpen5eImportDisposable != null && mOpen5eImportDisposable.isDisposed()) {
|
||||
break;
|
||||
Callable<Integer> importTask;
|
||||
if (source.importType == ImportSource.ImportType.OPEN5E_API) {
|
||||
importTask = () -> {
|
||||
int totalImported = 0;
|
||||
String nextUrl = null;
|
||||
do {
|
||||
if (mImportDisposable != null && mImportDisposable.isDisposed()) break;
|
||||
Open5eApiWrapper.Open5ePageResult page = Open5eApiWrapper.fetchPage(nextUrl);
|
||||
for (Monster monster : page.monsters) {
|
||||
if (mImportDisposable != null && mImportDisposable.isDisposed()) break;
|
||||
try {
|
||||
((MonsterCardsApplication) getApplication()).getMonsterRepository()
|
||||
.saveMonster(monster)
|
||||
.blockingAwait();
|
||||
totalImported++;
|
||||
} catch (Exception e) {
|
||||
Logger.logError("Failed to save monster to database", e);
|
||||
runOnUiThread(() -> ToastHelper.showShort(MainActivity.this, "An error occurred while saving a monster."));
|
||||
}
|
||||
}
|
||||
try {
|
||||
((MonsterCardsApplication) getApplication()).getMonsterRepository()
|
||||
.saveMonster(monster)
|
||||
.blockingAwait();
|
||||
totalImported++;
|
||||
} catch (Exception e) {
|
||||
Logger.logError("Failed to save monster to database", e);
|
||||
runOnUiThread(() -> ToastHelper.showShort(MainActivity.this, "An error occurred while saving a monster."));
|
||||
}
|
||||
}
|
||||
nextUrl = page.nextUrl;
|
||||
} while (nextUrl != null && !nextUrl.isEmpty());
|
||||
return totalImported;
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
snackbar.dismiss();
|
||||
SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_complete);
|
||||
}, throwable -> {
|
||||
snackbar.dismiss();
|
||||
Logger.logError("Failed to import all Open5e monsters", throwable);
|
||||
SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_failed);
|
||||
});
|
||||
}
|
||||
|
||||
public void importFromGitRepository(@NonNull GitRepositorySource source) {
|
||||
if (mGitImportDisposable != null && !mGitImportDisposable.isDisposed()) {
|
||||
ToastHelper.showShort(this, "A GitHub import is already running.");
|
||||
return;
|
||||
nextUrl = page.nextUrl;
|
||||
} while (nextUrl != null && !nextUrl.isEmpty());
|
||||
return totalImported;
|
||||
};
|
||||
} else {
|
||||
importTask = () -> GitRepoImporterService.importFromGitRepository(getApplicationContext(), source, () -> mImportDisposable != null && mImportDisposable.isDisposed());
|
||||
}
|
||||
|
||||
View rootView = findViewById(android.R.id.content);
|
||||
Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, R.string.snackbar_importing_github);
|
||||
snackbar.show();
|
||||
|
||||
mGitImportDisposable = Single.fromCallable(() ->
|
||||
GitRepoImporterService.importFromGitRepository(getApplicationContext(), source)
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(resultCount -> {
|
||||
snackbar.dismiss();
|
||||
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_github_import_complete, resultCount, source.projectName));
|
||||
}, throwable -> {
|
||||
snackbar.dismiss();
|
||||
Logger.logError("Failed to import from GitHub repository: " + source.projectName, throwable);
|
||||
SnackbarHelper.showLong(rootView, R.string.snackbar_github_import_failed);
|
||||
});
|
||||
mImportDisposable = Single.fromCallable(importTask)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(resultCount -> {
|
||||
snackbar.dismiss();
|
||||
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_complete, resultCount, source.projectName));
|
||||
}, throwable -> {
|
||||
snackbar.dismiss();
|
||||
Logger.logError("Failed to import from source: " + source.projectName, throwable);
|
||||
SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_failed, source.projectName));
|
||||
});
|
||||
}
|
||||
|
||||
public void importMultipleFilesFromUris(@NonNull List<Uri> uris) {
|
||||
@@ -464,11 +457,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) {
|
||||
mOpen5eImportDisposable.dispose();
|
||||
}
|
||||
if (mGitImportDisposable != null && !mGitImportDisposable.isDisposed()) {
|
||||
mGitImportDisposable.dispose();
|
||||
if (mImportDisposable != null && !mImportDisposable.isDisposed()) {
|
||||
mImportDisposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.majinnaibu.monstercards.data;
|
||||
|
||||
import com.majinnaibu.monstercards.models.GitRepositorySource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GitRepositoryConfig {
|
||||
public static final List<GitRepositorySource> SOURCES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
SOURCES.add(new GitRepositorySource(
|
||||
"pf2e_foundry",
|
||||
"Pathfinder 2e Foundry VTT",
|
||||
"foundryvtt",
|
||||
"https://github.com/foundryvtt/pf2e",
|
||||
"https://github.com/foundryvtt/pf2e/archive/refs/heads/master.zip",
|
||||
"com.majinnaibu.monstercards.importers.Pf2eImporter",
|
||||
".json"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.majinnaibu.monstercards.data;
|
||||
|
||||
import com.majinnaibu.monstercards.models.ImportSource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ImportConfig {
|
||||
public static final List<ImportSource> SOURCES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
SOURCES.add(new ImportSource(
|
||||
"open5e",
|
||||
"Open5e.com System Reference Document",
|
||||
"Open5e",
|
||||
"https://open5e.com",
|
||||
ImportSource.ImportType.OPEN5E_API,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
SOURCES.add(new ImportSource(
|
||||
"pf2e_foundry",
|
||||
"Pathfinder 2e Foundry VTT",
|
||||
"foundryvtt",
|
||||
"https://github.com/foundryvtt/pf2e",
|
||||
ImportSource.ImportType.GIT_ARCHIVE,
|
||||
"https://github.com/foundryvtt/pf2e/archive/refs/heads/master.zip",
|
||||
"com.majinnaibu.monstercards.importers.Pf2eImporter",
|
||||
".json"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||
import com.majinnaibu.monstercards.models.GitRepositorySource;
|
||||
import com.majinnaibu.monstercards.models.ImportSource;
|
||||
import com.majinnaibu.monstercards.models.Monster;
|
||||
import com.majinnaibu.monstercards.utils.Logger;
|
||||
|
||||
@@ -24,23 +24,28 @@ import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
public class GitRepoImporterService {
|
||||
|
||||
public static int importFromGitRepository(Context context, GitRepositorySource source) throws Exception {
|
||||
public static int importFromGitRepository(Context context, ImportSource source, BooleanSupplier isCancelled) throws Exception {
|
||||
int totalImported = 0;
|
||||
|
||||
File tempZip = File.createTempFile("repo_download", ".zip", context.getCacheDir());
|
||||
File extractDir = new File(context.getCacheDir(), "repo_extracted_" + System.currentTimeMillis());
|
||||
|
||||
try {
|
||||
downloadZip(source.downloadUrl, tempZip);
|
||||
downloadZip(source.downloadUrl, tempZip, isCancelled);
|
||||
|
||||
if (isCancelled.getAsBoolean()) return 0;
|
||||
|
||||
if (!extractDir.exists()) {
|
||||
extractDir.mkdirs();
|
||||
}
|
||||
|
||||
unzipAndFilter(tempZip, extractDir, source.fileExtension);
|
||||
unzipAndFilter(tempZip, extractDir, source.fileExtension, isCancelled);
|
||||
|
||||
if (isCancelled.getAsBoolean()) return 0;
|
||||
|
||||
// Instantiate Importer dynamically
|
||||
Class<?> clazz = Class.forName(source.importerClassName);
|
||||
@@ -50,7 +55,7 @@ public class GitRepoImporterService {
|
||||
MonsterRepository repository = ((MonsterCardsApplication) context.getApplicationContext()).getMonsterRepository();
|
||||
|
||||
// Process files
|
||||
totalImported = processDirectory(extractDir, importer, repository);
|
||||
totalImported = processDirectory(extractDir, importer, repository, isCancelled);
|
||||
|
||||
} finally {
|
||||
deleteFileOrDir(tempZip);
|
||||
@@ -60,7 +65,7 @@ public class GitRepoImporterService {
|
||||
return totalImported;
|
||||
}
|
||||
|
||||
private static void downloadZip(String urlStr, File dest) throws IOException {
|
||||
private static void downloadZip(String urlStr, File dest, BooleanSupplier isCancelled) throws IOException {
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
@@ -86,27 +91,29 @@ public class GitRepoImporterService {
|
||||
byte[] buffer = new byte[8192];
|
||||
int count;
|
||||
while ((count = in.read(buffer)) != -1) {
|
||||
if (isCancelled.getAsBoolean()) break;
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void unzipAndFilter(File zipFile, File extractDir, String filterExtension) throws IOException {
|
||||
private static void unzipAndFilter(File zipFile, File extractDir, String filterExtension, BooleanSupplier isCancelled) throws IOException {
|
||||
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
if (isCancelled.getAsBoolean()) break;
|
||||
if (!entry.isDirectory()) {
|
||||
String name = entry.getName();
|
||||
if (filterExtension == null || name.endsWith(filterExtension)) {
|
||||
File outFile = new File(extractDir, new File(name).getName());
|
||||
if (outFile.exists()) {
|
||||
// in case there are multiple files with same name in different folders
|
||||
outFile = new File(extractDir, System.currentTimeMillis() + "_" + new File(name).getName());
|
||||
}
|
||||
try (FileOutputStream fos = new FileOutputStream(outFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int count;
|
||||
while ((count = zis.read(buffer)) != -1) {
|
||||
if (isCancelled.getAsBoolean()) break;
|
||||
fos.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
@@ -117,12 +124,13 @@ public class GitRepoImporterService {
|
||||
}
|
||||
}
|
||||
|
||||
private static int processDirectory(File dir, EntityImporter<Monster> importer, MonsterRepository repository) {
|
||||
private static int processDirectory(File dir, EntityImporter<Monster> importer, MonsterRepository repository, BooleanSupplier isCancelled) {
|
||||
int importedCount = 0;
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) return 0;
|
||||
|
||||
for (File file : files) {
|
||||
if (isCancelled.getAsBoolean()) break;
|
||||
if (file.isFile()) {
|
||||
String content = readFileContent(file);
|
||||
if (content != null && importer.canImport(content)) {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.majinnaibu.monstercards.models;
|
||||
|
||||
public class GitRepositorySource {
|
||||
public String id;
|
||||
public String projectName;
|
||||
public String creatorGithubName;
|
||||
public String creatorPageLink;
|
||||
public String downloadUrl;
|
||||
public String importerClassName;
|
||||
public String fileExtension;
|
||||
|
||||
public GitRepositorySource(String id, String projectName, String creatorGithubName, String creatorPageLink, String downloadUrl, String importerClassName, String fileExtension) {
|
||||
this.id = id;
|
||||
this.projectName = projectName;
|
||||
this.creatorGithubName = creatorGithubName;
|
||||
this.creatorPageLink = creatorPageLink;
|
||||
this.downloadUrl = downloadUrl;
|
||||
this.importerClassName = importerClassName;
|
||||
this.fileExtension = fileExtension;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.majinnaibu.monstercards.models;
|
||||
|
||||
public class ImportSource {
|
||||
public enum ImportType {
|
||||
GIT_ARCHIVE,
|
||||
OPEN5E_API
|
||||
}
|
||||
|
||||
public String id;
|
||||
public String projectName;
|
||||
public String creatorName;
|
||||
public String creatorPageLink;
|
||||
public ImportType importType;
|
||||
|
||||
// Optional attributes specific to Git archive
|
||||
public String downloadUrl;
|
||||
public String importerClassName;
|
||||
public String fileExtension;
|
||||
|
||||
public ImportSource(String id, String projectName, String creatorName, String creatorPageLink, ImportType importType, String downloadUrl, String importerClassName, String fileExtension) {
|
||||
this.id = id;
|
||||
this.projectName = projectName;
|
||||
this.creatorName = creatorName;
|
||||
this.creatorPageLink = creatorPageLink;
|
||||
this.importType = importType;
|
||||
this.downloadUrl = downloadUrl;
|
||||
this.importerClassName = importerClassName;
|
||||
this.fileExtension = fileExtension;
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,8 @@ import com.majinnaibu.monstercards.utils.Logger;
|
||||
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||
import com.majinnaibu.monstercards.utils.ToastHelper;
|
||||
|
||||
import com.majinnaibu.monstercards.data.GitRepositoryConfig;
|
||||
import com.majinnaibu.monstercards.models.GitRepositorySource;
|
||||
import com.majinnaibu.monstercards.data.ImportConfig;
|
||||
import com.majinnaibu.monstercards.models.ImportSource;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
@@ -159,24 +159,17 @@ public class MCFragment extends Fragment {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
view.findViewById(R.id.button_import_open5e).setOnClickListener(v -> {
|
||||
if (getActivity() instanceof MainActivity) {
|
||||
((MainActivity) getActivity()).importAllFromOpen5e();
|
||||
}
|
||||
view.findViewById(R.id.button_import_source).setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
view.findViewById(R.id.button_import_github).setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
showGithubRepoPicker();
|
||||
showSourcePicker();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void showGithubRepoPicker() {
|
||||
private void showSourcePicker() {
|
||||
Context context = requireContext();
|
||||
List<GitRepositorySource> sources = GitRepositoryConfig.SOURCES;
|
||||
List<ImportSource> sources = ImportConfig.SOURCES;
|
||||
if (sources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -187,11 +180,11 @@ public class MCFragment extends Fragment {
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.dialog_select_github_repo)
|
||||
.setTitle(R.string.dialog_select_import_source)
|
||||
.setItems(repoNames, (d, which) -> {
|
||||
GitRepositorySource selected = sources.get(which);
|
||||
ImportSource selected = sources.get(which);
|
||||
if (getActivity() instanceof MainActivity) {
|
||||
((MainActivity) getActivity()).importFromGitRepository(selected);
|
||||
((MainActivity) getActivity()).startImportFromSource(selected);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.dialog_cancel, null)
|
||||
|
||||
Reference in New Issue
Block a user