From aed4d21a4fd173b7db385cb58220aa7cdd3b124a Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 13 Jun 2024 15:36:29 +0200 Subject: [PATCH] android: prepare transition to sdk 30+. cache last savestate path Migrate home folder to externalFilesDir (/sdcard/Android/data/com.flycast.emulator/files) if running on android 10+. Use externalFilesDir for home by default. No more onboarding on first install. Use content uri instead of hacking out real path since the latter won't work on higher target sdk. Cache last readable savestate path in hostfs::getSavestatePath(). All content folders are scanned when not found, which is very slow with ASS --- core/oslib/oslib.cpp | 16 +- .../com/flycast/emulator/AndroidStorage.java | 10 +- .../com/flycast/emulator/BaseGLActivity.java | 150 +++++++++++++++++- .../flycast/emulator/NativeGLActivity.java | 16 +- .../com/flycast/emulator/config/Config.java | 1 - 5 files changed, 179 insertions(+), 14 deletions(-) diff --git a/core/oslib/oslib.cpp b/core/oslib/oslib.cpp index 976620a30..fd61df6f8 100644 --- a/core/oslib/oslib.cpp +++ b/core/oslib/oslib.cpp @@ -128,10 +128,22 @@ std::string getSavestatePath(int index, bool writable) state_file = state_file + index_str + ".state"; if (index == -1) state_file += ".net"; - if (writable) + + static std::string lastFile; + static std::string lastPath; + + if (writable) { + lastFile.clear(); return get_writable_data_path(state_file); + } else - return get_readonly_data_path(state_file); + { + if (lastFile != state_file) { + lastFile = state_file; + lastPath = get_readonly_data_path(state_file); + } + return lastPath; + } } std::string getShaderCachePath(const std::string& filename) diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java index 0858f9928..df1fc3899 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java @@ -71,11 +71,15 @@ public class AndroidStorage { else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) activity.getContentResolver().takePersistableUriPermission(uri, storageIntentPerms); + /* Use the uri path now to avoid issues when targeting sdk 30+ in the future String realPath = getRealPath(uri); - if (realPath != null) + // when targeting sdk 30+ (android 11+) using the real path doesn't work (empty content) -> *must* use the uri + int targetSdkVersion = activity.getApplication().getApplicationInfo().targetSdkVersion; + if (realPath != null && targetSdkVersion <= Build.VERSION_CODES.Q) addStorageCallback(realPath); else - addStorageCallback(uri.toString()); + */ + addStorageCallback(uri.toString()); } } @@ -160,7 +164,7 @@ public class AndroidStorage { intent = Intent.createChooser(intent, "Select a cheat file"); } else { - intent = Intent.createChooser(intent, "Select a directory"); + intent = Intent.createChooser(intent, "Select a content directory"); } storageIntentPerms = Intent.FLAG_GRANT_READ_URI_PERMISSION | (writeAccess ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION : 0); intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | storageIntentPerms); diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java index 9c9e91619..1d02e1dff 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java @@ -3,6 +3,7 @@ package com.flycast.emulator; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; +import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -26,7 +27,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; -import com.flycast.emulator.AndroidStorage; import com.flycast.emulator.config.Config; import com.flycast.emulator.emu.AudioBackend; import com.flycast.emulator.emu.HttpClient; @@ -35,6 +35,9 @@ import com.flycast.emulator.periph.InputDeviceManager; import com.flycast.emulator.periph.SipEmulator; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -91,8 +94,21 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat. OuyaController.init(this); new HttpClient().nativeInit(); - String home_directory = prefs.getString(Config.pref_home, ""); - String result = JNIdc.initEnvironment((Emulator)getApplicationContext(), getFilesDir().getAbsolutePath(), home_directory, + String homeDir = prefs.getString(Config.pref_home, ""); + // Check that home dir is valid, migrate if needed + String newHome = checkHomeDirectory(homeDir); + if (newHome != null) { + if (!newHome.equals(homeDir)) + prefs.edit().putString(Config.pref_home, newHome).apply(); + finishCreation(); + } + Log.i("flycast", "BaseGLActivity.onCreate done"); + } + + protected void finishCreation() + { + String homeDir = prefs.getString(Config.pref_home, getDefaultHomeDir()); + String result = JNIdc.initEnvironment((Emulator)getApplicationContext(), getFilesDir().getAbsolutePath(), homeDir, Locale.getDefault().toString()); if (result != null) { AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); @@ -158,7 +174,7 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat. pendingIntentUrl = gameUri.toString(); } } - Log.i("flycast", "BaseGLActivity.onCreate done"); + Log.i("flycast", "BaseGLActivity.finishCreation done"); } private void setStorageDirectories() @@ -395,6 +411,132 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat. } + private String getDefaultHomeDir() + { + return getExternalFilesDir(null).getAbsolutePath(); + } + + private String checkHomeDirectory(String homeDir) + { + if (homeDir.isEmpty()) + // home dir not set: use default + return getDefaultHomeDir(); + if (homeDir.startsWith(getDefaultHomeDir())) + // home dir is ok + return homeDir; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) + // no need to migrate on Android 9 or earlier + return homeDir; + // Only ask to migrate once + String migrationPref = "legacy-storage-migration-done"; + if (prefs.getBoolean(migrationPref, false)) + return homeDir; + // Ask the user if he wants to migrate + AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); + dlgAlert.setMessage("The current Flycast home folder will be inaccessible in future versions.\n\n" + + "Do you want to move config and save files to a valid location?"); + dlgAlert.setTitle("Migrate Home"); + dlgAlert.setPositiveButton("Yes", + (dialog, id) -> BaseGLActivity.this.migrateHome(homeDir)); + dlgAlert.setNegativeButton("No", + (dialog, id) -> BaseGLActivity.this.finishCreation()); + dlgAlert.setIcon(android.R.drawable.ic_dialog_alert); + dlgAlert.setCancelable(false); + dlgAlert.create().show(); + // Don't ask again + prefs.edit().putBoolean(migrationPref, true).apply(); + + return null; + } + + private boolean migrationThreadCancelled = false; + private void migrateHome(String oldHome) + { + File source = new File(oldHome); + File dest = new File(getDefaultHomeDir()); + ProgressDialog progress = ProgressDialog.show(this, "Migrating", "Moving files to their new home", + true, true, dialogInterface -> migrationThreadCancelled = true); + progress.show(); + + migrationThreadCancelled = false; + Thread thread = new Thread(new Runnable() { + private void moveFile(File file, File toDir) + { + //Log.d("flycast", "Moving " + file.getAbsolutePath() + " to " + toDir.getAbsolutePath()); + try { + File dest = new File(toDir, file.getName()); + // file.renameTo(dest) doesn't seem to work + FileInputStream in = new FileInputStream(file); + FileOutputStream out = new FileOutputStream(dest); + byte[] buf = new byte[8192]; + while (true) { + int len = in.read(buf); + if (len == -1) + break; + out.write(buf, 0, len); + } + out.close(); + in.close(); + file.delete(); + } catch (IOException e) { + Log.e("flycast", "Error moving " + file.getAbsolutePath(), e); + } + } + + private void moveDir(File from, File to) + { + //Log.d("flycast", "Moving dir " + from.getAbsolutePath() + " to " + to.getAbsolutePath()); + if (!from.exists()) + return; + File[] files = from.listFiles(); + if (files == null) { + Log.e("flycast", "Can't list content of " + from.getAbsolutePath()); + return; + } + for (File file : files) + { + if (migrationThreadCancelled) + break; + if (file.isFile()) + moveFile(file, to); + else if (file.isDirectory() && !file.getName().equals("boxart")) { + File subDir = new File(to, file.getName()); + subDir.mkdir(); + moveDir(file, subDir); + } + } + from.delete(); + } + + private void migrate() + { + moveFile(new File(source, "emu.cfg"), dest); + if (migrationThreadCancelled) + return; + File mappings = new File(dest, "mappings"); + mappings.mkdirs(); + moveDir(new File(source, "mappings"), mappings); + if (migrationThreadCancelled) + return; + File data = new File(dest, "data"); + data.mkdirs(); + moveDir(new File(source, "data"), data); + } + + @Override + public void run() + { + migrate(); + runOnUiThread(() -> { + prefs.edit().putString(Config.pref_home, getDefaultHomeDir()).apply(); + progress.dismiss(); + BaseGLActivity.this.finishCreation(); + }); + } + }); + thread.start(); + } + // Called from native code public void onGameStateChange(boolean started) { runOnUiThread(new Runnable() { diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java index b62656235..86408aceb 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java @@ -35,28 +35,36 @@ public final class NativeGLActivity extends BaseGLActivity { super.onCreate(savedInstanceState); + Log.i("flycast", "NativeGLActivity.onCreate done"); + } + + protected void finishCreation() + { + super.finishCreation(); // Create the actual GL view mView = new NativeGLView(this); mLayout = new RelativeLayout(this); mLayout.addView(mView); setContentView(mLayout); - Log.i("flycast", "NativeGLActivity.onCreate done"); + Log.i("flycast", "NativeGLActivity.finishCreation done"); } @Override protected void doPause() { - mView.pause(); + if (mView != null) + mView.pause(); } @Override protected void doResume() { - mView.resume(); + if (mView != null) + mView.resume(); } @Override public boolean isSurfaceReady() { - return mView.isSurfaceReady(); + return mView != null && mView.isSurfaceReady(); } // Called from native code diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/config/Config.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/config/Config.java index fc3ef8601..ab59afc5f 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/config/Config.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/config/Config.java @@ -2,5 +2,4 @@ package com.flycast.emulator.config; public class Config { public static final String pref_home = "home_directory"; - public static String git_issues = "https://github.com/flyinghead/flycast/issues/"; }