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
This commit is contained in:
Flyinghead 2024-06-13 15:36:29 +02:00
parent 9b4667cb38
commit aed4d21a4f
5 changed files with 179 additions and 14 deletions

View File

@ -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)

View File

@ -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);

View File

@ -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() {

View File

@ -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

View File

@ -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/";
}