From 9818ad48914f5cfea894572ff3b53c1371ade6f4 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Mon, 21 Jun 2021 01:40:47 +0200 Subject: [PATCH] [Android] Locate external App data path and migrate data files --- Source/init.cpp | 12 +- android-project/app/build.gradle | 4 +- .../app/src/main/AndroidManifest.xml | 2 +- .../devilutionx/DevilutionXSDLActivity.java | 291 ++++++++++-------- 4 files changed, 159 insertions(+), 150 deletions(-) diff --git a/Source/init.cpp b/Source/init.cpp index 783c343a0..e380b1c9c 100644 --- a/Source/init.cpp +++ b/Source/init.cpp @@ -138,22 +138,12 @@ void init_cleanup() void init_archives() { std::vector paths; - paths.reserve(5); -#ifdef __ANDROID__ - paths.push_back(std::string(getenv("EXTERNAL_STORAGE")) + "/devilutionx/"); -#else paths.push_back(paths::BasePath()); -#endif paths.push_back(paths::PrefPath()); if (paths[0] == paths[1]) paths.pop_back(); -#ifdef __ANDROID__ - if (getenv("SECONDARY_STORAGE") != nullptr) - paths.emplace_back(std::string(getenv("SECONDARY_STORAGE")) + "/devilutionx/"); - if (getenv("EXTERNAL_SDCARD_STORAGE") != nullptr) - paths.emplace_back(std::string(getenv("EXTERNAL_SDCARD_STORAGE")) + "/devilutionx/"); -#elif defined(__linux__) +#if defined(__linux__) && !defined(__ANDROID__) paths.emplace_back("/usr/share/diasurgical/devilutionx/"); paths.emplace_back("/usr/local/share/diasurgical/devilutionx/"); #elif defined(__3DS__) diff --git a/android-project/app/build.gradle b/android-project/app/build.gradle index 033e93d64..fb50e11b5 100644 --- a/android-project/app/build.gradle +++ b/android-project/app/build.gradle @@ -8,7 +8,7 @@ else { } android { - compileSdkVersion 29 + compileSdkVersion 29 // Upgrade to 30 after november 2021 aaptOptions { noCompress 'mpq' } @@ -17,7 +17,7 @@ android { applicationId "org.diasurgical.devilutionx" } minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 29 // Upgrade to 30 after november 2021 versionCode 18 versionName "1.2.1" externalNativeBuild { diff --git a/android-project/app/src/main/AndroidManifest.xml b/android-project/app/src/main/AndroidManifest.xml index e672d7743..80f5f96b8 100644 --- a/android-project/app/src/main/AndroidManifest.xml +++ b/android-project/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ android:name="android.hardware.type.pc" android:required="false" /> - + diff --git a/android-project/app/src/main/java/org/diasurgical/devilutionx/DevilutionXSDLActivity.java b/android-project/app/src/main/java/org/diasurgical/devilutionx/DevilutionXSDLActivity.java index 05d8fb494..a7d3f4633 100644 --- a/android-project/app/src/main/java/org/diasurgical/devilutionx/DevilutionXSDLActivity.java +++ b/android-project/app/src/main/java/org/diasurgical/devilutionx/DevilutionXSDLActivity.java @@ -1,136 +1,155 @@ -package org.diasurgical.devilutionx; - -import android.Manifest; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.preference.PreferenceManager; - -import org.libsdl.app.SDLActivity; - -import java.io.File; - -public class DevilutionXSDLActivity extends SDLActivity { - final String permission = Manifest.permission.READ_EXTERNAL_STORAGE; - private Handler permissionHandler; - private AlertDialog PermissionDialog; - private final String SharewareTitle = "Shareware version"; - private SharedPreferences mPrefs; - final private String shouldShowInstallationGuidePref = "shouldShowInstallationGuide"; - - protected void onCreate(Bundle savedInstanceState) { - checkStoragePermission(); - - super.onCreate(savedInstanceState); - } - - private boolean shouldShowInstallationGuide(boolean isDataAccessible) { - mPrefs = PreferenceManager.getDefaultSharedPreferences(this); - return mPrefs.getBoolean(shouldShowInstallationGuidePref, !isDataAccessible); - } - - private void installationGuideShown() { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putBoolean(shouldShowInstallationGuidePref, false); - editor.apply(); - } - - private void checkStoragePermission() { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { - checkStoragePermissionLollipop(); - return; - } - - if (PackageManager.PERMISSION_GRANTED != checkSelfPermission(permission)) { - requestPermissions(new String[]{permission}, 1); - } - - boolean dataFileFound = hasDataFile(); - boolean shouldShowInstallationGuide = shouldShowInstallationGuide(dataFileFound && PackageManager.PERMISSION_GRANTED == checkSelfPermission(permission)); - - if (!shouldShowInstallationGuide && !shouldShowRequestPermissionRationale(permission)) { - return; - } - - String message = "To play the full version you must grant the application read access to " + System.getenv("EXTERNAL_STORAGE") + "."; - if (!dataFileFound) { - message = "To play the full version you must place DIABDAT.MPQ from the original game in " + System.getenv("EXTERNAL_STORAGE") + " and grant the application read access.\n\nIf you don't have the original game then you can buy Diablo from GOG.com."; - installationGuideShown(); - } - - permissionHandler = new Handler(); - - PermissionDialog = new AlertDialog.Builder(this) - .setTitle(SharewareTitle) - .setPositiveButton("OK", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - permissionHandler.post(new Runnable() { - @Override - public void run() { - throw new RuntimeException(); - } - }); - } - }) - .setMessage(message) - .show(); - - if (dataFileFound) { - triggerPulse(); // Start full game as soon as we have file access - } - - try { - Looper.loop(); - } catch (RuntimeException ignored) { - } - } - - private void checkStoragePermissionLollipop() { - boolean dataFileFound = hasDataFile(); - boolean shouldShowInstallationGuide = shouldShowInstallationGuide(dataFileFound); - if (!shouldShowInstallationGuide) { - return; - } - - String message = "To play the full version you must place DIABDAT.MPQ from the original game in " + System.getenv("EXTERNAL_STORAGE") + ".\n\nIf you don't have the original game then you can buy Diablo from GOG.com."; - new AlertDialog.Builder(this) - .setTitle(SharewareTitle) - .setPositiveButton("OK", null) - .setMessage(message) - .show(); - installationGuideShown(); - } - - private void triggerPulse() { - permissionHandler.postDelayed(new Runnable() { - @Override - public void run() { - if (PackageManager.PERMISSION_GRANTED == checkSelfPermission(permission)) { - PermissionDialog.hide(); - throw new RuntimeException(); - } - triggerPulse(); - } - }, 50); - } - - private boolean hasDataFile() { - File fileLower = new File(System.getenv("EXTERNAL_STORAGE") + "/diabdat.mpq"); - File fileUpper = new File(System.getenv("EXTERNAL_STORAGE") + "/DIABDAT.MPQ"); - - return fileUpper.exists() || fileLower.exists(); - } - - protected String[] getLibraries() { - return new String[]{ - "SDL2", - "SDL2_ttf", - "devilutionx" - }; - } -} +package org.diasurgical.devilutionx; + +import android.Manifest; +import android.app.AlertDialog; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.libsdl.app.SDLActivity; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class DevilutionXSDLActivity extends SDLActivity { + private final String SharewareTitle = "Shareware version"; + private SharedPreferences mPrefs; + final private String shouldShowInstallationGuidePref = "shouldShowInstallationGuide"; + private String externalDir; + + protected void onCreate(Bundle savedInstanceState) { + externalDir = getExternalFilesDir(null).getAbsolutePath(); + + migrateAppData(); + + if (shouldShowInstallationGuide()) { + showInstallationGuide(); + } + + super.onCreate(savedInstanceState); + } + + /** + * Check if this is the first time the app is run and the full game data is not already present + */ + private boolean shouldShowInstallationGuide() { + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + return mPrefs.getBoolean(shouldShowInstallationGuidePref, !hasFullGameData()); + } + + private boolean hasFullGameData() { + File fileLower = new File(externalDir + "/diabdat.mpq"); + File fileUpper = new File(externalDir + "/DIABDAT.MPQ"); + + return fileUpper.exists() || fileLower.exists(); + } + + private void installationGuideShown() { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putBoolean(shouldShowInstallationGuidePref, false); + editor.apply(); + } + + private void showInstallationGuide() { + String message = "To play the full version you must place DIABDAT.MPQ from the original game in Android" + + externalDir.split("/Android")[1] + + ".\n\nIf you don't have the original game then you can buy Diablo from GOG.com."; + new AlertDialog.Builder(this) + .setTitle(SharewareTitle) + .setPositiveButton("OK", null) + .setMessage(message) + .show(); + installationGuideShown(); + } + + private boolean copyFile(File src, File dst) { + try { + try (InputStream in = new FileInputStream(src)) { + try (OutputStream out = new FileOutputStream(dst)) { + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + + return true; + } + } + } catch (IOException exception) { + Log.e("copyFile", exception.getMessage()); + } + + return false; + } + + private void migrateFile(File file) { + if (!file.exists() || !file.canRead()) { + return; + } + File newPath = new File(externalDir + "/" + file.getName()); + if (newPath.exists()) { + if (file.canWrite()) { + file.delete(); + } + return; + } + if (!new File(newPath.getParent()).canWrite()) { + return; + } + if (!file.renameTo(newPath)) { + if (copyFile(file, newPath) && file.canWrite()) { + file.delete(); + } + } + } + + /** + * This can be removed Nov 2021 and Google will no longer permit access to the old folder from that point on + */ + private void migrateAppData() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { + if (PackageManager.PERMISSION_GRANTED != checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + return; + } + } + + migrateFile(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/diabdat.mpq")); + migrateFile(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/DIABDAT.MPQ")); + + migrateFile(new File("/sdcard/diabdat.mpq")); + migrateFile(new File("/sdcard/devilutionx/diabdat.mpq")); + migrateFile(new File("/sdcard/devilutionx/spawn.mpq")); + + for (File internalFile : getFilesDir().listFiles()) { + migrateFile(internalFile); + } + } + + protected String[] getArguments() { + return new String[]{ + "--data-dir", + externalDir, + "--config-dir", + externalDir, + "--save-dir", + externalDir + }; + } + + protected String[] getLibraries() { + return new String[]{ + "SDL2", + "SDL2_ttf", + "devilutionx" + }; + } +}