From 8e727633145d0979d9405611e1d618ec4d3fe614 Mon Sep 17 00:00:00 2001 From: OMGeeky Date: Mon, 17 Oct 2022 23:20:16 +0200 Subject: [PATCH 01/14] Import Export --- .../activity/LoadSaveActivity.java | 839 ++++++--- .../StartScreenActivity_MainMenu.java | 118 +- .../rpg/AndorsTrail/controller/Constants.java | 4 + .../rpg/AndorsTrail/savegames/Savegames.java | 221 +-- .../rpg/AndorsTrail/util/AndroidStorage.java | 107 +- .../AndorsTrail/view/CustomDialogFactory.java | 11 + AndorsTrail/res/layout/loadsave.xml | 52 +- AndorsTrail/res/values/strings.xml | 1576 +++++++++-------- 8 files changed, 1740 insertions(+), 1188 deletions(-) diff --git a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/activity/LoadSaveActivity.java b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/activity/LoadSaveActivity.java index 2c92e8910..884355625 100644 --- a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/activity/LoadSaveActivity.java +++ b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/activity/LoadSaveActivity.java @@ -1,16 +1,26 @@ package com.gpl.rpg.AndorsTrail.activity; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; import android.app.Dialog; +import android.content.ClipData; +import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.support.annotation.RequiresApi; +import android.support.v4.provider.DocumentFile; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -22,241 +32,644 @@ import android.widget.Toast; import com.gpl.rpg.AndorsTrail.AndorsTrailApplication; import com.gpl.rpg.AndorsTrail.AndorsTrailPreferences; import com.gpl.rpg.AndorsTrail.R; +import com.gpl.rpg.AndorsTrail.controller.Constants; import com.gpl.rpg.AndorsTrail.model.ModelContainer; import com.gpl.rpg.AndorsTrail.resource.tiles.TileManager; import com.gpl.rpg.AndorsTrail.savegames.Savegames; import com.gpl.rpg.AndorsTrail.savegames.Savegames.FileHeader; +import com.gpl.rpg.AndorsTrail.util.AndroidStorage; import com.gpl.rpg.AndorsTrail.util.ThemeHelper; import com.gpl.rpg.AndorsTrail.view.CustomDialogFactory; public final class LoadSaveActivity extends AndorsTrailBaseActivity implements OnClickListener { - private boolean isLoading = true; - private static final int SLOT_NUMBER_CREATE_NEW_SLOT = -1; - private static final int SLOT_NUMBER_FIRST_SLOT = 1; - private ModelContainer model; - private TileManager tileManager; - private AndorsTrailPreferences preferences; + private boolean isLoading = true; + //region special slot numbers + private static final int SLOT_NUMBER_CREATE_NEW_SLOT = -1; + public static final int SLOT_NUMBER_EXPORT_SAVEGAMES = -2; + public static final int SLOT_NUMBER_IMPORT_SAVEGAMES = -3; + public static final int SLOT_NUMBER_IMPORT_WORLDMAP = -4; + private static final int SLOT_NUMBER_FIRST_SLOT = 1; + //endregion + private ModelContainer model; + private TileManager tileManager; + private AndorsTrailPreferences preferences; - @Override - public void onCreate(Bundle savedInstanceState) { - setTheme(ThemeHelper.getDialogTheme()); - super.onCreate(savedInstanceState); + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(ThemeHelper.getDialogTheme()); + super.onCreate(savedInstanceState); - final AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this); - app.setWindowParameters(this); - this.model = app.getWorld().model; - this.preferences = app.getPreferences(); - this.tileManager = app.getWorld().tileManager; + final AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this); + app.setWindowParameters(this); + this.model = app.getWorld().model; + this.preferences = app.getPreferences(); + this.tileManager = app.getWorld().tileManager; - String loadsave = getIntent().getData().getLastPathSegment(); - isLoading = (loadsave.equalsIgnoreCase("load")); + String loadsave = getIntent().getData().getLastPathSegment(); + isLoading = (loadsave.equalsIgnoreCase("load")); - setContentView(R.layout.loadsave); + setContentView(R.layout.loadsave); - TextView tv = (TextView) findViewById(R.id.loadsave_title); - if (isLoading) { - tv.setCompoundDrawablesWithIntrinsicBounds(android.R.drawable.ic_menu_search, 0, 0, 0); - tv.setText(R.string.loadsave_title_load); - } else { - tv.setCompoundDrawablesWithIntrinsicBounds(android.R.drawable.ic_menu_save, 0, 0, 0); - tv.setText(R.string.loadsave_title_save); - } + TextView tv = (TextView) findViewById(R.id.loadsave_title); + if (isLoading) { + tv.setCompoundDrawablesWithIntrinsicBounds(android.R.drawable.ic_menu_search, 0, 0, 0); + tv.setText(R.string.loadsave_title_load); + } else { + tv.setCompoundDrawablesWithIntrinsicBounds(android.R.drawable.ic_menu_save, 0, 0, 0); + tv.setText(R.string.loadsave_title_save); + } - ViewGroup slotList = (ViewGroup) findViewById(R.id.loadsave_slot_list); - Button slotTemplateButton = (Button) findViewById(R.id.loadsave_slot_n); - LayoutParams params = slotTemplateButton.getLayoutParams(); - slotList.removeView(slotTemplateButton); + ViewGroup slotList = (ViewGroup) findViewById(R.id.loadsave_slot_list); + Button slotTemplateButton = (Button) findViewById(R.id.loadsave_slot_n); + LayoutParams params = slotTemplateButton.getLayoutParams(); + slotList.removeView(slotTemplateButton); - ViewGroup newSlotContainer = (ViewGroup) findViewById(R.id.loadsave_save_to_new_slot_container); - Button createNewSlot = (Button) findViewById(R.id.loadsave_save_to_new_slot); + ViewGroup newSlotContainer = (ViewGroup) findViewById(R.id.loadsave_save_to_new_slot_container); + Button createNewSlot = (Button) findViewById(R.id.loadsave_save_to_new_slot); - addSavegameSlotButtons(slotList, params, Savegames.getUsedSavegameSlots(this)); + Button exportSaves = (Button) findViewById(R.id.loadsave_export_save); + Button importSaves = (Button) findViewById(R.id.loadsave_import_save); + Button importWorldmap = (Button) findViewById(R.id.loadsave_import_worldmap); - checkAndRequestPermissions(); - - if (!isLoading) { - createNewSlot.setTag(SLOT_NUMBER_CREATE_NEW_SLOT); - createNewSlot.setOnClickListener(this); - newSlotContainer.setVisibility(View.VISIBLE); - } else { - newSlotContainer.setVisibility(View.GONE); - } - } + exportSaves.setTag(SLOT_NUMBER_EXPORT_SAVEGAMES); + importSaves.setTag(SLOT_NUMBER_IMPORT_SAVEGAMES); + importWorldmap.setTag(SLOT_NUMBER_IMPORT_WORLDMAP); - private static final int READ_EXTERNAL_STORAGE_REQUEST=1; - private static final int WRITE_EXTERNAL_STORAGE_REQUEST=2; - - @TargetApi(23) - private void checkAndRequestPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - if (getApplicationContext().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - this.requestPermissions(new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, READ_EXTERNAL_STORAGE_REQUEST); - } - if (getApplicationContext().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - this.requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE_REQUEST); - } - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) { - Toast.makeText(this, R.string.storage_permissions_mandatory, Toast.LENGTH_LONG).show(); - ((AndorsTrailApplication)getApplication()).discardWorld(); - finish(); - } - } - - private void addSavegameSlotButtons(ViewGroup parent, LayoutParams params, List usedSavegameSlots) { - int unused = 1; - for (int slot : usedSavegameSlots) { - final FileHeader header = Savegames.quickload(this, slot); - if (header == null) continue; - - while (unused < slot){ - Button b = new Button(this); - b.setLayoutParams(params); - b.setTag(unused); - b.setOnClickListener(this); - b.setText(getString(R.string.loadsave_empty_slot, unused)); - tileManager.setImageViewTileForPlayer(getResources(), b, header.iconID); - parent.addView(b, params); - unused++; - } - unused++; - - Button b = new Button(this); - b.setLayoutParams(params); - b.setTag(slot); - b.setOnClickListener(this); - b.setText(slot + ". " + header.describe()); - tileManager.setImageViewTileForPlayer(getResources(), b, header.iconID); - parent.addView(b, params); - } - } - - public void loadsave(int slot) { - if (slot == SLOT_NUMBER_CREATE_NEW_SLOT) { - List usedSlots = Savegames.getUsedSavegameSlots(this); - if (usedSlots.isEmpty()) slot = SLOT_NUMBER_FIRST_SLOT; - else slot = Collections.max(usedSlots) + 1; - } - if (slot < SLOT_NUMBER_FIRST_SLOT) slot = SLOT_NUMBER_FIRST_SLOT; - - Intent i = new Intent(); - i.putExtra("slot", slot); - setResult(Activity.RESULT_OK, i); - LoadSaveActivity.this.finish(); - } - - private String getConfirmOverwriteQuestion(int slot) { - if (isLoading) return null; - if (slot == SLOT_NUMBER_CREATE_NEW_SLOT) return null; // if we're creating a new slot - if (!Savegames.getSlotFile(slot, this).exists()) return null; - - if (preferences.displayOverwriteSavegame == AndorsTrailPreferences.CONFIRM_OVERWRITE_SAVEGAME_ALWAYS) { - return getString(R.string.loadsave_save_overwrite_confirmation_all); - } - if (preferences.displayOverwriteSavegame == AndorsTrailPreferences.CONFIRM_OVERWRITE_SAVEGAME_NEVER) { - return null; - } - - final String currentPlayerName = model.player.getName(); - final FileHeader header = Savegames.quickload(this, slot); - if (header == null) return null; - - final String savedPlayerName = header.playerName; - if (currentPlayerName.equals(savedPlayerName)) return null; // if the names match - - return getString(R.string.loadsave_save_overwrite_confirmation, savedPlayerName, currentPlayerName); - } - - @Override - public void onClick(View view) { - final int slot = (Integer) view.getTag(); - - if (!isLoading && slot != SLOT_NUMBER_CREATE_NEW_SLOT && AndorsTrailApplication.CURRENT_VERSION == AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION) { - final FileHeader header = Savegames.quickload(this, slot); - if (header != null && header.fileversion != AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION) { - final Dialog d = CustomDialogFactory.createDialog(this, - "Overwriting not allowed", - getResources().getDrawable(android.R.drawable.ic_dialog_alert), - "You are currently using a development version of Andor's trail. Overwriting a regular savegame is not allowed in development mode.", - null, - true); - CustomDialogFactory.addDismissButton(d, android.R.string.ok); - CustomDialogFactory.show(d); - return; - } - } + ViewGroup exportImportContainer = (ViewGroup) findViewById(R.id.loadsave_export_import_save_container); - if (isLoading) { - if(!Savegames.getSlotFile(slot, this).exists()) { - showErrorLoadingEmptySlot(); - } else { - final FileHeader header = Savegames.quickload(this, slot); - if (header != null && !header.hasUnlimitedSaves) { - showSlotGetsDeletedOnLoadWarning(slot); - } else { - loadsave(slot); - } - } - } else { - final String message = getConfirmOverwriteQuestion(slot); - if (message != null) { - showConfirmoverwriteQuestion(slot, message); - } else { - loadsave(slot); - } - } - } + addSavegameSlotButtons(slotList, params, Savegames.getUsedSavegameSlots(this)); - private void showErrorLoadingEmptySlot() { - final Dialog d = CustomDialogFactory.createDialog(this, - getString(R.string.startscreen_error_loading_game), - getResources().getDrawable(android.R.drawable.ic_dialog_alert), - getString(R.string.startscreen_error_loading_empty_slot), - null, - true); - CustomDialogFactory.addDismissButton(d, android.R.string.ok); - CustomDialogFactory.show(d); - } + checkAndRequestPermissions(); - private void showSlotGetsDeletedOnLoadWarning(final int slot) { - final Dialog d = CustomDialogFactory.createDialog(this, - getString(R.string.startscreen_attention_slot_gets_delete_on_load), - getResources().getDrawable(android.R.drawable.ic_dialog_alert), - getString(R.string.startscreen_attention_message_slot_gets_delete_on_load), - null, - true); - CustomDialogFactory.addButton(d, android.R.string.ok, new View.OnClickListener() { - @Override - public void onClick(View v) { - loadsave(slot); - } - }); - CustomDialogFactory.show(d); - } + if (!isLoading) { + createNewSlot.setTag(SLOT_NUMBER_CREATE_NEW_SLOT); + createNewSlot.setOnClickListener(this); + newSlotContainer.setVisibility(View.VISIBLE); + exportImportContainer.setVisibility(View.GONE); + } else { + newSlotContainer.setVisibility(View.GONE); - private void showConfirmoverwriteQuestion(final int slot, String message) { - final String title = - getString(R.string.loadsave_save_overwrite_confirmation_title) + ' ' - + getString(R.string.loadsave_save_overwrite_confirmation_slot, slot); - final Dialog d = CustomDialogFactory.createDialog(this, - title, - getResources().getDrawable(android.R.drawable.ic_dialog_alert), - message, - null, - true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + exportSaves.setOnClickListener(this); + importSaves.setOnClickListener(this); + importWorldmap.setOnClickListener(this); + exportImportContainer.setVisibility(View.VISIBLE); + } + } + } + + private static final int READ_EXTERNAL_STORAGE_REQUEST = 1; + private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 2; + + private void checkAndRequestPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + if (getApplicationContext().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + this.requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, READ_EXTERNAL_STORAGE_REQUEST); + } + if (getApplicationContext().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + this.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE_REQUEST); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, R.string.storage_permissions_mandatory, Toast.LENGTH_LONG).show(); + ((AndorsTrailApplication) getApplication()).discardWorld(); + finish(); + } + } + + private void addSavegameSlotButtons(ViewGroup parent, LayoutParams params, List usedSavegameSlots) { + int unused = 1; + for (int slot : usedSavegameSlots) { + final FileHeader header = Savegames.quickload(this, slot); + if (header == null) continue; + + while (unused < slot) { + Button b = new Button(this); + b.setLayoutParams(params); + b.setTag(unused); + b.setOnClickListener(this); + b.setText(getString(R.string.loadsave_empty_slot, unused)); + tileManager.setImageViewTileForPlayer(getResources(), b, header.iconID); + parent.addView(b, params); + unused++; + } + unused++; + + Button b = new Button(this); + b.setLayoutParams(params); + b.setTag(slot); + b.setOnClickListener(this); + b.setText(slot + ". " + header.describe()); + tileManager.setImageViewTileForPlayer(getResources(), b, header.iconID); + parent.addView(b, params); + } + } + + + private void completeLoadSaveActivity(int slot) { + Intent i = new Intent(); + if (slot == SLOT_NUMBER_CREATE_NEW_SLOT) { + List usedSlots = Savegames.getUsedSavegameSlots(this); + if (usedSlots.isEmpty()) + slot = SLOT_NUMBER_FIRST_SLOT; + else slot = Collections.max(usedSlots) + 1; + } else if (slot == SLOT_NUMBER_EXPORT_SAVEGAMES + || slot == SLOT_NUMBER_IMPORT_SAVEGAMES + || slot == SLOT_NUMBER_IMPORT_WORLDMAP) { + i.putExtra("import_export", true); + } else if (slot < SLOT_NUMBER_FIRST_SLOT) + slot = SLOT_NUMBER_FIRST_SLOT; + + i.putExtra("slot", slot); + setResult(Activity.RESULT_OK, i); + LoadSaveActivity.this.finish(); + } + + private String getConfirmOverwriteQuestion(int slot) { + if (isLoading) + return null; + + return getConfirmOverwriteQuestionIgnoringLoading(slot); + } + + private String getConfirmOverwriteQuestionIgnoringLoading(int slot) { + if (slot == SLOT_NUMBER_CREATE_NEW_SLOT) + return null;//creating a new savegame + + if (!Savegames.getSlotFile(slot, this).exists()) + return null;//nothing in slot to overwrite + + if (preferences.displayOverwriteSavegame == AndorsTrailPreferences.CONFIRM_OVERWRITE_SAVEGAME_ALWAYS) { + return getString(R.string.loadsave_save_overwrite_confirmation_all); + } + if (preferences.displayOverwriteSavegame == AndorsTrailPreferences.CONFIRM_OVERWRITE_SAVEGAME_NEVER) { + return null; + } + + final String currentPlayerName = model.player.getName(); + final FileHeader header = Savegames.quickload(this, slot); + if (header == null) return null; + + final String savedPlayerName = header.playerName; + if (currentPlayerName.equals(savedPlayerName)) return null; //if the names match + + return getString(R.string.loadsave_save_overwrite_confirmation, savedPlayerName, currentPlayerName); + } + + @Override + public void onClick(View view) { + final int slot = (Integer) view.getTag(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + switch (slot) { + case SLOT_NUMBER_IMPORT_WORLDMAP: + clickImportWorldmap(); + return; + case SLOT_NUMBER_IMPORT_SAVEGAMES: + clickImportSaveGames(); + return; + case SLOT_NUMBER_EXPORT_SAVEGAMES: + clickExportSaveGames(); + return; + } + } + if (!isLoading + && slot != SLOT_NUMBER_CREATE_NEW_SLOT + && AndorsTrailApplication.CURRENT_VERSION == AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION) { + if (!isOverwriteTargetInIncompatibleVersion(slot)) { + saveOrOverwriteSavegame(slot); + } + } else if (isLoading) { + loadSaveGame(slot); + } else { + saveOrOverwriteSavegame(slot); + } + } + + private void saveOrOverwriteSavegame(int slot) { + final String message = getConfirmOverwriteQuestion(slot); + if (message != null) { + showConfirmoverwriteQuestion(slot, message); + } else { + completeLoadSaveActivity(slot); + } + } + + private boolean isOverwriteTargetInIncompatibleVersion(int slot) { + final FileHeader header = Savegames.quickload(this, slot); + if (header != null && header.fileversion != AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION) { + final Dialog d = CustomDialogFactory.createErrorDialog(this, "Overwriting not allowed", "You are currently using a development version of Andor's trail. Overwriting a regular savegame is not allowed in development mode."); + CustomDialogFactory.show(d); + return true; + } + return false; + } + + //region Imports/Exports + + private void exportSaveGames(Intent data) { + Uri uri = data.getData(); + + Context context = getApplicationContext(); + ContentResolver resolver = AndorsTrailApplication.getApplicationFromActivity(this).getContentResolver(); + + File storageDir = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY); + DocumentFile source = DocumentFile.fromFile(storageDir); + DocumentFile target = DocumentFile.fromTreeUri(context, uri); + if (target == null) { + return; + } + + DocumentFile[] files = source.listFiles(); + + boolean hasExistingFiles = false; + for (DocumentFile file : + files) { + String fileName = file.getName(); + if (fileName == null) + continue; + + DocumentFile existingFile = target.findFile(fileName); + if (existingFile != null) { + hasExistingFiles = true; + break; + } + } + + if (hasExistingFiles) { + showConfirmOverwriteByExportQuestion(resolver, target, files); + } else { + exportSaveGamesFolderContentToFolder(resolver, target, files); + } + } + + private void exportSaveGamesFolderContentToFolder(ContentResolver resolver, DocumentFile target, DocumentFile[] files) { + for (DocumentFile file : files) { + String fileName = file.getName(); + DocumentFile existingFile = target.findFile(fileName); + boolean hasExistingFile = existingFile != null && existingFile.exists(); + + if (file.isFile()) { + try { + if (hasExistingFile) + existingFile.delete(); + + + AndroidStorage.copyDocumentFileToNewOrExistingFile(file, resolver, target); + } catch (IOException e) { + e.printStackTrace(); + } + } else if (file.isDirectory()) { + DocumentFile targetWorlmap = existingFile; + //if the folder exists already, put the files in the existing folder. (should not happen because of check earlier) + if (!hasExistingFile) + //create a new folder for the worldmap-files + targetWorlmap = target.createDirectory(Constants.FILENAME_WORLDMAP_DIRECTORY); + + if (targetWorlmap == null)//Unable to create worldmap folder for some reason + continue; + + DocumentFile[] worldmapFiles = file.listFiles(); + for (DocumentFile f : worldmapFiles) { + try { + AndroidStorage.copyDocumentFileToNewOrExistingFile(f, resolver, targetWorlmap); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + completeLoadSaveActivity(SLOT_NUMBER_EXPORT_SAVEGAMES); + } + + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + private void importSaveGames(Intent data) { + Uri uri = data.getData(); + ClipData uris = data.getClipData(); + + if (uri == null && uris == null) { + //no file was selected + return; + } + + Context context = getApplicationContext(); + ContentResolver resolver = context.getContentResolver(); + + File storageDir = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY); + DocumentFile appSavegameFolder = DocumentFile.fromFile(storageDir); + + List uriList = new ArrayList<>(); + if (uri != null) { + uriList.add(uri); + } else { + for (int i = 0; i < uris.getItemCount(); i++) + uriList.add(uris.getItemAt(i).getUri()); + } + importSaveGamesFromUris(context, resolver, appSavegameFolder, uriList); + } + + private void importSaveGamesFromUris(Context context, ContentResolver resolver, DocumentFile appSavegameFolder, List uriList) { + int count = uriList.size(); + + ArrayList alreadyExistingFiles = new ArrayList<>(); + ArrayList newFiles = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + Uri item = uriList.get(i); + DocumentFile itemFile = DocumentFile.fromSingleUri(context, item); + boolean fileAlreadyExists = getExistsSavegameInOwnFiles(itemFile, appSavegameFolder); + if (fileAlreadyExists) + alreadyExistingFiles.add(itemFile); + else + newFiles.add(itemFile); + } + + if (alreadyExistingFiles.size() > 0) { + showConfirmOverwriteByImportQuestion(resolver, appSavegameFolder, alreadyExistingFiles, newFiles); + } else { + importSaveGames(resolver, appSavegameFolder, newFiles); + } + } + + private void importSaveGames(ContentResolver resolver, DocumentFile appSavegameFolder, List saveFiles) { + for (DocumentFile file : saveFiles) { + int slot = getSlotFromSavegameFileName(file.getName()); + importSaveGameFile(resolver, appSavegameFolder, file, slot); + } + } + + + private void completeSavegameImportAndCheckIfDone(List importsNeedingConfirmation, int slot) { + importsNeedingConfirmation.remove((Object) slot); + if (importsNeedingConfirmation.isEmpty()) { + completeLoadSaveActivity(SLOT_NUMBER_IMPORT_SAVEGAMES); + } + } + + private boolean getExistsSavegameInOwnFiles(DocumentFile savegameFile, DocumentFile appSavegameFolder) { + if (savegameFile == null) + return false; + + DocumentFile foundFile = appSavegameFolder.findFile(Objects.requireNonNull(savegameFile.getName())); + return foundFile != null && foundFile.exists(); + } + + private int getSlotFromSavegameFileName(String fileName) { + if (fileName == null || !fileName.startsWith(Constants.FILENAME_SAVEGAME_FILENAME_PREFIX)) { + //TODO: Maybe output a message that the file didn't have the right name? + return -1; + } + String slotStr = fileName.substring(Constants.FILENAME_SAVEGAME_FILENAME_PREFIX.length()); + + int slot; + try { + slot = Integer.parseInt(slotStr); + return slot; + } catch (NumberFormatException e) { + //TODO: Maybe output a message that the file didn't have the right name? + return -1; + } + } + + private void importSaveGameFile(ContentResolver resolver, DocumentFile appSavegameFolder, DocumentFile itemFile, int slot) { + String targetName = Savegames.getSlotFileName(slot); + DocumentFile targetFile = getOrCreateDocumentFile(appSavegameFolder, targetName); + + if (targetFile == null || !targetName.equals(targetFile.getName())) { + showErrorImportingSaveGameUnknown();//TODO: maybe replace with a more specific error message + return; + } + + try { + AndroidStorage.copyDocumentFile(itemFile, resolver, targetFile); + } catch (IOException e) { + showErrorImportingSaveGameUnknown(); + e.printStackTrace(); + } + } + + private DocumentFile getOrCreateDocumentFile(DocumentFile folder, String targetName) { + DocumentFile targetFile = folder.findFile(targetName);//try finding the file + if (targetFile == null)//no file found, creating new one + targetFile = folder.createFile(Constants.NO_FILE_EXTENSION_MIME_TYPE, targetName); + return targetFile; + } + + private void importWorldmap(Intent data) { + Uri uri = data.getData(); + + Context context = getApplicationContext(); + ContentResolver resolver = AndorsTrailApplication.getApplicationFromActivity(this).getContentResolver(); + + File storageDir = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY); + DocumentFile storageFolder = DocumentFile.fromFile(storageDir); + DocumentFile ownWorldmapFolder = storageFolder.findFile(Constants.FILENAME_WORLDMAP_DIRECTORY); + if (ownWorldmapFolder == null) { + ownWorldmapFolder = storageFolder.createDirectory(Constants.FILENAME_WORLDMAP_DIRECTORY); + } + + DocumentFile chosenFolder = DocumentFile.fromTreeUri(context, uri); + if (chosenFolder == null || !chosenFolder.isDirectory()) { + showErrorImportingWorldmapWrongDirectory(); + return; + } + if (!Constants.FILENAME_WORLDMAP_DIRECTORY.equals(chosenFolder.getName())) { + //user did not select the worldmap folder directly + DocumentFile file = chosenFolder.findFile(Constants.FILENAME_WORLDMAP_DIRECTORY); + if (file == null || !file.isDirectory() || !Constants.FILENAME_WORLDMAP_DIRECTORY.equals(file.getName())) { + //could not find a worldmap folder in the users selection + showErrorImportingWorldmapWrongDirectory(); + return; + } + + chosenFolder = file; + } + + DocumentFile[] files = chosenFolder.listFiles(); + for (DocumentFile file : files) { + if (file.isFile()) { + try { + AndroidStorage.copyDocumentFileToNewOrExistingFile(file, resolver, ownWorldmapFolder); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + completeLoadSaveActivity(SLOT_NUMBER_IMPORT_WORLDMAP); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void clickExportSaveGames() { + startActivityForResult(AndroidStorage.getNewOpenDirectoryIntent(), -SLOT_NUMBER_EXPORT_SAVEGAMES); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void clickImportSaveGames() { + startActivityForResult(AndroidStorage.getNewSelectMultipleSavegameFilesIntent(), -SLOT_NUMBER_IMPORT_SAVEGAMES); + + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void clickImportWorldmap() { + startActivityForResult(AndroidStorage.getNewOpenDirectoryIntent(), -SLOT_NUMBER_IMPORT_WORLDMAP); + + } + + private void showConfirmOverwriteByExportQuestion(ContentResolver resolver, DocumentFile targetFolder, DocumentFile[] files) { + final Dialog d = CustomDialogFactory.createDialog(this, + getString(R.string.loadsave_export_overwrite_confirmation_title), + getResources().getDrawable(android.R.drawable.ic_dialog_alert), + getString(R.string.loadsave_export_overwrite_confirmation), + null, + true); + + CustomDialogFactory.addButton(d, android.R.string.yes, v -> exportSaveGamesFolderContentToFolder(resolver, targetFolder, files)); + CustomDialogFactory.addDismissButton(d, android.R.string.no); + + CustomDialogFactory.show(d); + } + + private void showConfirmOverwriteByImportQuestion(ContentResolver resolver, + DocumentFile appSavegameFolder, + List alreadyExistingFiles, + List newFiles) { + final String title = getString(R.string.loadsave_import_overwrite_confirmation_title); + String message = getString(R.string.loadsave_import_overwrite_confirmation); + + StringBuilder sb = new StringBuilder(); + sb.append('\n'); + int amount = alreadyExistingFiles.size(); + + Context context = AndorsTrailApplication.getApplicationFromActivity(this).getApplicationContext(); + + for (int i = 0; i < amount && i < 3; i++) { + DocumentFile alreadyExistingFile = alreadyExistingFiles.get(i); + String alreadyExistingFileName = alreadyExistingFile.getName(); + FileHeader fileHeader = Savegames.quickload(context, getSlotFromSavegameFileName(alreadyExistingFileName)); + sb.append('\n'); + String fileHeaderDesription = ""; + if (fileHeader != null) + fileHeaderDesription = fileHeader.describe(); + + sb.append(getString(R.string.loadsave_import_overwrite_confirmation_file_pattern, alreadyExistingFileName, fileHeaderDesription)); +// sb.append(alreadyExistingFile.getName()); + } + if (amount > 3) { + sb.append("\n..."); + } + message = message + sb; + final Dialog d = CustomDialogFactory.createDialog(this, + title, + getResources().getDrawable(android.R.drawable.ic_dialog_alert), + message, + null, + true); + + CustomDialogFactory.addButton(d, android.R.string.yes, v -> newFiles.addAll(alreadyExistingFiles)); + CustomDialogFactory.addDismissButton(d, android.R.string.no); + CustomDialogFactory.setDismissListener(d, dialog -> { + importSaveGames(resolver, appSavegameFolder, newFiles); + completeLoadSaveActivity(SLOT_NUMBER_IMPORT_SAVEGAMES); + }); + CustomDialogFactory.show(d); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (resultCode != Activity.RESULT_OK) + return; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + switch (-requestCode) { + case SLOT_NUMBER_EXPORT_SAVEGAMES: + exportSaveGames(data); + return; + case SLOT_NUMBER_IMPORT_SAVEGAMES: + importSaveGames(data); + return; + case SLOT_NUMBER_IMPORT_WORLDMAP: + importWorldmap(data); + return; + } + } + + } + + //endregion + + private void loadSaveGame(int slot) { + if (!Savegames.getSlotFile(slot, this).exists()) { + showErrorLoadingEmptySlot(); + } else { + final FileHeader header = Savegames.quickload(this, slot); + if (header != null && !header.hasUnlimitedSaves) { + showSlotGetsDeletedOnLoadWarning(slot); + } else { + completeLoadSaveActivity(slot); + } + } + } + + //region show Dialogs + + private void showErrorImportingWorldmapWrongDirectory() { + final Dialog d = CustomDialogFactory.createErrorDialog(this, + getString(R.string.loadsave_import_worldmap_unsuccessfull), + getString(R.string.loadsave_import_worldmap_unsuccessfull_wrong_directory)); + CustomDialogFactory.show(d); + } + + private void showErrorImportingSaveGameUnknown() { + final Dialog d = CustomDialogFactory.createErrorDialog(this, + getString(R.string.loadsave_import_save_unsuccessfull), + getString(R.string.loadsave_import_save_unsuccessfull_unknown)); + CustomDialogFactory.show(d); + } + + private void showErrorLoadingEmptySlot() { + final Dialog d = CustomDialogFactory.createErrorDialog(this, + getString(R.string.startscreen_error_loading_game), + getString(R.string.startscreen_error_loading_empty_slot)); + CustomDialogFactory.show(d); + } + + private void showSlotGetsDeletedOnLoadWarning(final int slot) { + final Dialog d = CustomDialogFactory.createDialog(this, + getString(R.string.startscreen_attention_slot_gets_delete_on_load), + getResources().getDrawable(android.R.drawable.ic_dialog_alert), + getString(R.string.startscreen_attention_message_slot_gets_delete_on_load), + null, + true); + CustomDialogFactory.addButton(d, android.R.string.ok, v -> completeLoadSaveActivity(slot)); + CustomDialogFactory.show(d); + } + + private void showConfirmoverwriteQuestion(final int slot, String message) { + final String title = + getString(R.string.loadsave_save_overwrite_confirmation_title) + ' ' + + getString(R.string.loadsave_save_overwrite_confirmation_slot, slot); + final Dialog d = CustomDialogFactory.createDialog(this, + title, + getResources().getDrawable(android.R.drawable.ic_dialog_alert), + message, + null, + true); + + CustomDialogFactory.addButton(d, android.R.string.yes, v -> completeLoadSaveActivity(slot)); + CustomDialogFactory.addDismissButton(d, android.R.string.no); + CustomDialogFactory.show(d); + } + + //endregion - CustomDialogFactory.addButton(d, android.R.string.yes, new View.OnClickListener() { - @Override - public void onClick(View v) { - loadsave(slot); - } - }); - CustomDialogFactory.addDismissButton(d, android.R.string.no); - CustomDialogFactory.show(d); - } } diff --git a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/activity/fragment/StartScreenActivity_MainMenu.java b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/activity/fragment/StartScreenActivity_MainMenu.java index 92385a563..86ac78327 100644 --- a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/activity/fragment/StartScreenActivity_MainMenu.java +++ b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/activity/fragment/StartScreenActivity_MainMenu.java @@ -41,22 +41,22 @@ import com.gpl.rpg.AndorsTrail.view.CustomDialogFactory; public class StartScreenActivity_MainMenu extends Fragment { - private static final int INTENTREQUEST_PREFERENCES = 7; - public static final int INTENTREQUEST_LOADGAME = 9; + private static final int INTENTREQUEST_PREFERENCES = 7; + public static final int INTENTREQUEST_LOADGAME = 9; - private boolean hasExistingGame = false; - private Button startscreen_continue; - private Button startscreen_newgame; - private Button startscreen_load; - private ViewGroup save_preview_holder; - private ImageView save_preview_hero_icon; - private TextView save_preview_hero_desc; - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - updatePreferences(false); - super.onCreateView(inflater, container, savedInstanceState); + private boolean hasExistingGame = false; + private Button startscreen_continue; + private Button startscreen_newgame; + private Button startscreen_load; + private ViewGroup save_preview_holder; + private ImageView save_preview_hero_icon; + private TextView save_preview_hero_desc; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + updatePreferences(false); + super.onCreateView(inflater, container, savedInstanceState); if (container != null) { @@ -339,49 +339,51 @@ public class StartScreenActivity_MainMenu extends Fragment { } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - switch (requestCode) { - case INTENTREQUEST_LOADGAME: - if (resultCode != Activity.RESULT_OK) break; - final int slot = data.getIntExtra("slot", 1); - continueGame(false, slot, null); - break; - case INTENTREQUEST_PREFERENCES: - updatePreferences(true); - break; - } - } - - private void updatePreferences(boolean alreadyStartedLoadingResources) { - AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(getActivity()); - AndorsTrailPreferences preferences = app.getPreferences(); - preferences.read(getActivity()); - if (app.setLocale(getActivity())) { - if (alreadyStartedLoadingResources) { - // Changing the locale after having loaded the game requires resources to - // be re-loaded. Therefore, we just exit here. - Toast.makeText(getActivity(), R.string.change_locale_requires_restart, Toast.LENGTH_LONG).show(); - doFinish(); - return; - } - } - if (ThemeHelper.changeTheme(preferences.selectedTheme)) { - // Changing the theme requires a restart to re-create all activities. - Toast.makeText(getActivity(), R.string.change_theme_requires_restart, Toast.LENGTH_LONG).show(); - doFinish(); - return; - } - app.getWorld().tileManager.updatePreferences(preferences); - } - - @SuppressLint("NewApi") - private void doFinish() { - //For Lollipop and above - ((AndorsTrailApplication)getActivity().getApplication()).discardWorld(); - getActivity().finish(); - } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case INTENTREQUEST_LOADGAME: + if (resultCode != Activity.RESULT_OK) break; + final boolean wasImportOrExport = data.getBooleanExtra("import_export", false); + if (wasImportOrExport) break; + final int slot = data.getIntExtra("slot", 1); + continueGame(false, slot, null); + break; + case INTENTREQUEST_PREFERENCES: + updatePreferences(true); + break; + } + } + + private void updatePreferences(boolean alreadyStartedLoadingResources) { + AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(getActivity()); + AndorsTrailPreferences preferences = app.getPreferences(); + preferences.read(getActivity()); + if (app.setLocale(getActivity())) { + if (alreadyStartedLoadingResources) { + // Changing the locale after having loaded the game requires resources to + // be re-loaded. Therefore, we just exit here. + Toast.makeText(getActivity(), R.string.change_locale_requires_restart, Toast.LENGTH_LONG).show(); + doFinish(); + return; + } + } + if (ThemeHelper.changeTheme(preferences.selectedTheme)) { + // Changing the theme requires a restart to re-create all activities. + Toast.makeText(getActivity(), R.string.change_theme_requires_restart, Toast.LENGTH_LONG).show(); + doFinish(); + return; + } + app.getWorld().tileManager.updatePreferences(preferences); + } + + @SuppressLint("NewApi") + private void doFinish() { + //For Lollipop and above + ((AndorsTrailApplication) getActivity().getApplication()).discardWorld(); + getActivity().finish(); + } public interface OnNewGameRequestedListener { diff --git a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/controller/Constants.java b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/controller/Constants.java index 01e554c20..ca4767bbd 100644 --- a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/controller/Constants.java +++ b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/controller/Constants.java @@ -51,6 +51,10 @@ public final class Constants { public static final String CHEAT_DETECTION_FOLDER = "dEAGyGE3YojqXjI3x4x7"; public static final String PASSIVE_ACHIEVEMENT_CHECK_PHRASE = "passive_achievement_check"; + public static final String SAVEGAME_FILE_MIME_TYPE = "application/octet-stream"; + public static final String WORLDMAP_FILE_MIME_TYPE = "image/png"; + public static final String NO_FILE_EXTENSION_MIME_TYPE = "application/no_file_extension_mime_type"; + public static final Random rnd = new Random(); public static int rollValue(final ConstRange r) { return rollValue(r.max, r.current); } public static int rollValue(final ConstRange r, int bias) { return rollValue((r.max + 1) * 100 -1, r.current * 100 + bias)/100; } diff --git a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/savegames/Savegames.java b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/savegames/Savegames.java index ade6f155f..caf3567cd 100644 --- a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/savegames/Savegames.java +++ b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/savegames/Savegames.java @@ -86,7 +86,7 @@ public final class Savegames { private static void writeBackup(Context androidContext, byte[] savegame, String playerId) throws IOException { File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER); - if (!cheatDetectionFolder.exists()) cheatDetectionFolder.mkdir(); + ensureDirExists(cheatDetectionFolder); File backupFile = new File(cheatDetectionFolder, playerId + "X"); FileOutputStream fileOutputStream = new FileOutputStream(backupFile); fileOutputStream.write(savegame); @@ -127,34 +127,33 @@ public final class Savegames { } } - private static boolean triedToCheat(Context androidContext, FileHeader fh) throws IOException { - long savedVersionToCheck = 0; - File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER); - if (!cheatDetectionFolder.exists()) cheatDetectionFolder.mkdir(); - File cheatDetectionFile = new File(cheatDetectionFolder, fh.playerId); - if (cheatDetectionFile.exists()) { - FileInputStream fileInputStream = new FileInputStream(cheatDetectionFile); - DataInputStream dataInputStream = new DataInputStream(fileInputStream); - final CheatDetection cheatDetection = new CheatDetection(dataInputStream); - savedVersionToCheck = cheatDetection.savedVersion; - dataInputStream.close(); - fileInputStream.close(); - } + private static boolean triedToCheat(Context androidContext, FileHeader fh) throws IOException { + long savedVersionToCheck = 0; + File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER); + ensureDirExists(cheatDetectionFolder); + File cheatDetectionFile = new File(cheatDetectionFolder, fh.playerId); + if (cheatDetectionFile.exists()) { + FileInputStream fileInputStream = new FileInputStream(cheatDetectionFile); + DataInputStream dataInputStream = new DataInputStream(fileInputStream); + final CheatDetection cheatDetection = new CheatDetection(dataInputStream); + savedVersionToCheck = cheatDetection.savedVersion; + dataInputStream.close(); + fileInputStream.close(); + } if (savedVersionToCheck == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED) { return true; } - if (androidContext.getFileStreamPath(fh.playerId).exists()) { - FileInputStream fileInputStream = androidContext.openFileInput(fh.playerId); - DataInputStream dataInputStream = new DataInputStream(fileInputStream); - final CheatDetection cheatDetection = new CheatDetection(dataInputStream); - if (cheatDetection.savedVersion == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED) { - savedVersionToCheck = DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED; - } - else if (cheatDetection.savedVersion > savedVersionToCheck) { - savedVersionToCheck = cheatDetection.savedVersion; - } + if (androidContext.getFileStreamPath(fh.playerId).exists()) { + FileInputStream fileInputStream = androidContext.openFileInput(fh.playerId); + DataInputStream dataInputStream = new DataInputStream(fileInputStream); + final CheatDetection cheatDetection = new CheatDetection(dataInputStream); + if (cheatDetection.savedVersion == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED) { + savedVersionToCheck = DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED; + } else if (cheatDetection.savedVersion > savedVersionToCheck) { + savedVersionToCheck = cheatDetection.savedVersion; + } if (AndorsTrailApplication.DEVELOPMENT_DEBUGMESSAGES) { L.log("Internal cheatcheck file savedVersion: " + cheatDetection.savedVersion); @@ -167,31 +166,48 @@ public final class Savegames { return (savedVersionToCheck == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED || fh.savedVersion < savedVersionToCheck); } - private static FileOutputStream getOutputFile(Context androidContext, int slot) throws IOException { - if (slot == SLOT_QUICKSAVE) { - return androidContext.openFileOutput(Constants.FILENAME_SAVEGAME_QUICKSAVE, Context.MODE_PRIVATE); - } else { - ensureSavegameDirectoryExists(androidContext); - return new FileOutputStream(getSlotFile(slot, androidContext)); - } - } - private static void ensureSavegameDirectoryExists(Context context) { - File dir = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY); - if (!dir.exists()) dir.mkdir(); - } - private static FileInputStream getInputFile(Context androidContext, int slot) throws IOException { - if (slot == SLOT_QUICKSAVE) { - return androidContext.openFileInput(Constants.FILENAME_SAVEGAME_QUICKSAVE); - } else { - return new FileInputStream(getSlotFile(slot, androidContext)); - } - } + private static FileOutputStream getOutputFile(Context androidContext, int slot) throws IOException { + if (slot == SLOT_QUICKSAVE) { + return androidContext.openFileOutput(Constants.FILENAME_SAVEGAME_QUICKSAVE, Context.MODE_PRIVATE); + } else { + ensureSavegameDirectoryExists(androidContext); + return new FileOutputStream(getSlotFile(slot, androidContext)); + } + } - public static File getSlotFile(int slot, Context context) { - File root = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY); - return new File(root, Constants.FILENAME_SAVEGAME_FILENAME_PREFIX + slot); - } + private static void ensureSavegameDirectoryExists(Context context) { + File dir = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY); + ensureDirExists(dir); + } + public static boolean ensureDirExists(File dir) { + if (!dir.exists()) { + boolean worked = dir.mkdir(); + return worked; + } + return true; + } + + private static FileInputStream getInputFile(Context androidContext, int slot) throws IOException { + if (slot == SLOT_QUICKSAVE) { + return androidContext.openFileInput(Constants.FILENAME_SAVEGAME_QUICKSAVE); + } else { + return new FileInputStream(getSlotFile(slot, androidContext)); + } + } + + public static File getSlotFile(int slot, Context context) { + File root = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY); + return getSlotFile(slot, root); + } + + public static File getSlotFile(int slot, File directory) { + return new File(directory, getSlotFileName(slot)); + } + + public static String getSlotFileName(int slot) { + return Constants.FILENAME_SAVEGAME_FILENAME_PREFIX + slot; + } public static void saveWorld(WorldContext world, OutputStream outStream, String displayInfo) throws IOException { @@ -207,10 +223,11 @@ public final class Savegames { dest.close(); } - public static LoadSavegameResult loadWorld(Resources res, WorldContext world, ControllerContext controllers, InputStream inState, FileHeader fh) throws IOException { - DataInputStream src = new DataInputStream(inState); - final FileHeader header = new FileHeader(src, fh.skipIcon); - if (header.fileversion > AndorsTrailApplication.CURRENT_VERSION) return LoadSavegameResult.savegameIsFromAFutureVersion; + public static LoadSavegameResult loadWorld(Resources res, WorldContext world, ControllerContext controllers, InputStream inState, FileHeader fh) throws IOException { + DataInputStream src = new DataInputStream(inState); + final FileHeader header = new FileHeader(src, fh.skipIcon); + if (header.fileversion > AndorsTrailApplication.CURRENT_VERSION) + return LoadSavegameResult.savegameIsFromAFutureVersion; world.maps.readFromParcel(src, world, controllers, header.fileversion); world.model = new ModelContainer(src, world, controllers, header.fileversion); @@ -249,15 +266,15 @@ public final class Savegames { } } - private static void writeCheatCheck(Context androidContext, long savedVersion, String playerId) throws IOException { - File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER); - if (!cheatDetectionFolder.exists()) cheatDetectionFolder.mkdir(); - File cheatDetectionFile = new File(cheatDetectionFolder, playerId); - FileOutputStream fileOutputStream = new FileOutputStream(cheatDetectionFile); - DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); - CheatDetection.writeToParcel(dataOutputStream, savedVersion); - dataOutputStream.close(); - fileOutputStream.close(); + private static void writeCheatCheck(Context androidContext, long savedVersion, String playerId) throws IOException { + File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER); + ensureDirExists(cheatDetectionFolder); + File cheatDetectionFile = new File(cheatDetectionFolder, playerId); + FileOutputStream fileOutputStream = new FileOutputStream(cheatDetectionFile); + DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); + CheatDetection.writeToParcel(dataOutputStream, savedVersion); + dataOutputStream.close(); + fileOutputStream.close(); fileOutputStream = androidContext.openFileOutput(playerId, Context.MODE_PRIVATE); dataOutputStream = new DataOutputStream(fileOutputStream); @@ -268,26 +285,26 @@ public final class Savegames { private static final Pattern savegameFilenamePattern = Pattern.compile(Constants.FILENAME_SAVEGAME_FILENAME_PREFIX + "(\\d+)"); - public static List getUsedSavegameSlots(Context context) { - try { - final List result = new ArrayList(); - AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY).listFiles(new FilenameFilter() { - @Override - public boolean accept(File f, String filename) { - Matcher m = savegameFilenamePattern.matcher(filename); - if (m != null && m.matches()) { - result.add(Integer.parseInt(m.group(1))); - return true; - } - return false; - } - }); - Collections.sort(result); - return result; - } catch (Exception e) { - return null; - } - } + public static List getUsedSavegameSlots(Context context) { + try { + final List result = new ArrayList(); + AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY).listFiles(new FilenameFilter() { + @Override + public boolean accept(File f, String filename) { + Matcher m = savegameFilenamePattern.matcher(filename); + if (m != null && m.matches()) { + result.add(Integer.parseInt(m.group(1))); + return true; + } + return false; + } + }); + Collections.sort(result); + return result; + } catch (Exception e) { + return new ArrayList(); + } + } private static final class CheatDetection { public final int fileversion; @@ -307,17 +324,16 @@ public final class Savegames { } - - public static final class FileHeader { - public final int fileversion; - public final String playerName; - public final String displayInfo; - public final int iconID; - public boolean skipIcon = false; - public final boolean isDead; - public final boolean hasUnlimitedSaves; - public final String playerId; - public final long savedVersion; + public static final class FileHeader { + public final int fileversion; + public final String playerName; + public final String displayInfo; + public final int iconID; + public boolean skipIcon = false; + public final boolean isDead; + public final boolean hasUnlimitedSaves; + public final String playerId; + public final long savedVersion; public String describe() { return (fileversion == AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION ? "(D) " : "") + playerName + ", " + displayInfo; @@ -326,17 +342,18 @@ public final class Savegames { // ====== PARCELABLE =================================================================== - public FileHeader(DataInputStream src, boolean skipIcon) throws IOException { - int fileversion = src.readInt(); - if (fileversion == 11) fileversion = 5; // Fileversion 5 had no version identifier, but the first byte was 11. - this.fileversion = fileversion; - if (fileversion >= 14) { // Before fileversion 14 (0.6.7), we had no file header. - this.playerName = src.readUTF(); - this.displayInfo = src.readUTF(); - } else { - this.playerName = null; - this.displayInfo = null; - } + public FileHeader(DataInputStream src, boolean skipIcon) throws IOException { + int fileversion = src.readInt(); + if (fileversion == 11) + fileversion = 5; // Fileversion 5 had no version identifier, but the first byte was 11. + this.fileversion = fileversion; + if (fileversion >= 14) { // Before fileversion 14 (0.6.7), we had no file header. + this.playerName = src.readUTF(); + this.displayInfo = src.readUTF(); + } else { + this.playerName = null; + this.displayInfo = null; + } if (fileversion >= 43) { int id = src.readInt(); diff --git a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/util/AndroidStorage.java b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/util/AndroidStorage.java index 010ae2051..bc39ce210 100644 --- a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/util/AndroidStorage.java +++ b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/util/AndroidStorage.java @@ -1,10 +1,16 @@ package com.gpl.rpg.AndorsTrail.util; +import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Environment; +import android.support.annotation.NonNull; +import android.support.annotation.RequiresApi; import android.support.v4.content.FileProvider; +import android.support.v4.provider.DocumentFile; +import android.webkit.MimeTypeMap; import com.gpl.rpg.AndorsTrail.controller.Constants; @@ -17,7 +23,7 @@ import java.io.OutputStream; public final class AndroidStorage { public static File getStorageDirectory(Context context, String name) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return context.getExternalFilesDir(name); } else { @@ -25,6 +31,7 @@ public final class AndroidStorage { return new File(root, name); } } + public static boolean shouldMigrateToInternalStorage(Context context) { boolean ret = false; File externalSaveGameDirectory = new File(Environment.getExternalStorageDirectory(), Constants.FILENAME_SAVEGAME_DIRECTORY); @@ -76,35 +83,91 @@ public final class AndroidStorage { } } - private static void copyFile(File source, File target) throws IOException { - InputStream in = null; - OutputStream out = null; - try { - in = new FileInputStream(source); - out = new FileOutputStream(target); - byte[] buf = new byte[1024]; - int length; - while ((length = in.read(buf)) > 0) { - out.write(buf, 0, length); - } - } finally { - if (in != null) { - in.close(); - } - if (out != null) { - out.close(); - } + public static void copyFile(File source, File target) throws IOException { + try (InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(target)) { + copyStream(in, out); } } + public static void copyStream(InputStream in, OutputStream out) throws IOException { + byte[] buf = new byte[1024]; + int length; + while ((length = in.read(buf)) > 0) { + out.write(buf, 0, length); + } + } + + + public static void copyDocumentFileToNewOrExistingFile(DocumentFile sourceFile, ContentResolver resolver, DocumentFile targetFolder) throws IOException { + copyDocumentFileToNewOrExistingFile(sourceFile, resolver, targetFolder, Constants.NO_FILE_EXTENSION_MIME_TYPE); + } + + + public static void copyDocumentFileToNewOrExistingFile(DocumentFile sourceFile, ContentResolver resolver, DocumentFile targetFolder, String mimeType) throws IOException { + String fileName = sourceFile.getName(); + DocumentFile file = targetFolder.findFile(fileName); + if (file == null) + file = targetFolder.createFile(mimeType, fileName); + if (file == null) + return; + + AndroidStorage.copyDocumentFile(sourceFile, resolver, file); + } + + public static void copyDocumentFile(DocumentFile sourceFile, ContentResolver resolver, DocumentFile targetFile) throws IOException { + try (OutputStream outputStream = resolver.openOutputStream(targetFile.getUri()); + InputStream inputStream = resolver.openInputStream(sourceFile.getUri())) { + copyStream(inputStream, outputStream); + } + } + + + /** + * Gets the MIME-Type for a file.

+ * Fallback value is '* / *' (without spaces)

+ * Mostly copied together from: StackOverflow + */ + @NonNull + public static String getMimeType(ContentResolver resolver, Uri uri) { + String type = null; + if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + type = resolver.getType(uri); + return type; + } + + final String extension = MimeTypeMap.getFileExtensionFromUrl(uri.getPath()); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase()); + } + if (type == null) { + type = "*/*"; // fallback type. + } + return type; + } + public static String getUrlForFile(Context context, File worldmap) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { String applicationId = context.getPackageName(); -// Uri uri = FileProvider.getUriForFile(context, "com.gpl.rpg.AndorsTrail.fileprovider", worldmap); - Uri uri = FileProvider.getUriForFile(context, applicationId + ".fileprovider", worldmap); + Uri uri = FileProvider.getUriForFile(context, applicationId + ".fileprovider", worldmap);//TODO: remove fixed fileprovider return uri.toString(); } else { return "file://" + worldmap.getAbsolutePath(); } } + + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public static Intent getNewOpenDirectoryIntent() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + return intent; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public static Intent getNewSelectMultipleSavegameFilesIntent() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + intent.setType(Constants.SAVEGAME_FILE_MIME_TYPE); + return intent; + } } diff --git a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/view/CustomDialogFactory.java b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/view/CustomDialogFactory.java index d554f2885..c1d22c5cf 100644 --- a/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/view/CustomDialogFactory.java +++ b/AndorsTrail/app/src/main/java/com/gpl/rpg/AndorsTrail/view/CustomDialogFactory.java @@ -88,6 +88,17 @@ public class CustomDialogFactory { return dialog; } + + public static CustomDialog createErrorDialog(final Context context, String title, String description) { + final CustomDialog d = createDialog(context, + title, + context.getResources().getDrawable(android.R.drawable.ic_dialog_alert), + description, + null, + true); + CustomDialogFactory.addDismissButton(d, android.R.string.ok); + return d; + } public static CustomDialog setTitle(final CustomDialog dialog, String title, Drawable icon) { TextView titleView = (TextView) dialog.findViewById(R.id.dialog_title); diff --git a/AndorsTrail/res/layout/loadsave.xml b/AndorsTrail/res/layout/loadsave.xml index 204b245b6..b4261518c 100644 --- a/AndorsTrail/res/layout/loadsave.xml +++ b/AndorsTrail/res/layout/loadsave.xml @@ -51,19 +51,45 @@ +