diff --git a/pkg/android/phoenix/AndroidManifest.xml b/pkg/android/phoenix/AndroidManifest.xml
index dafdc7e2e6..2d978a820d 100644
--- a/pkg/android/phoenix/AndroidManifest.xml
+++ b/pkg/android/phoenix/AndroidManifest.xml
@@ -24,6 +24,9 @@
android:banner="@drawable/banner"
android:extractNativeLibs="true"
tools:ignore="UnusedAttribute">
+
diff --git a/pkg/android/phoenix/res/values/strings.xml b/pkg/android/phoenix/res/values/strings.xml
new file mode 100644
index 0000000000..c8e55b6caf
--- /dev/null
+++ b/pkg/android/phoenix/res/values/strings.xml
@@ -0,0 +1,31 @@
+
+
+
+ Migrate RetroArch Folder
+
+
+ Because RetroArch was updated, the location of the RetroArch folder has changed. \n
+ Would you like to import data from an existing RetroArch folder?
+
+
+ Yes, select existing RetroArch folder
+
+
+ No, don\'t ask again
+
+
+ No, ask next time
+
+
+ Copying RetroArch Files…
+
+
+ Your RetroArch folder has been migrated. \n
+ You can find it in the files app under RetroArch > User Data.
+
+
+ Your RetroArch folder has been migrated. \n
+ You can find it in the files app under RetroArch > User Data. \n
+ There were errors copying some files.
+
+
diff --git a/pkg/android/phoenix/src/com/retroarch/browser/mainmenu/MainMenuActivity.java b/pkg/android/phoenix/src/com/retroarch/browser/mainmenu/MainMenuActivity.java
index bc555a74f9..cc3f37636d 100644
--- a/pkg/android/phoenix/src/com/retroarch/browser/mainmenu/MainMenuActivity.java
+++ b/pkg/android/phoenix/src/com/retroarch/browser/mainmenu/MainMenuActivity.java
@@ -12,14 +12,6 @@ import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.provider.Settings;
-import java.util.List;
-import java.util.ArrayList;
-import android.content.pm.PackageManager;
-import android.Manifest;
-import android.content.DialogInterface;
-import android.app.AlertDialog;
-import android.util.Log;
-
/**
* {@link PreferenceActivity} subclass that provides all of the
* functionality of the main menu screen.
@@ -27,6 +19,7 @@ import android.util.Log;
public final class MainMenuActivity extends PreferenceActivity
{
public static String PACKAGE_NAME;
+ final int REQUEST_CODE_START = 120;
public void finalStartup()
{
@@ -63,6 +56,14 @@ public final class MainMenuActivity extends PreferenceActivity
retro.putExtra("EXTERNAL", external);
}
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data)
+ {
+ if(requestCode == REQUEST_CODE_START) {
+ finalStartup();
+ }
+ }
+
@Override
public void onCreate(Bundle savedInstanceState)
{
@@ -75,6 +76,7 @@ public final class MainMenuActivity extends PreferenceActivity
UserPreferences.updateConfigFile(this);
- finalStartup();
+ Intent i = new Intent(this, MigrateRetroarchFolderActivity.class);
+ startActivityForResult(i, REQUEST_CODE_START);
}
}
diff --git a/pkg/android/phoenix/src/com/retroarch/browser/mainmenu/MigrateRetroarchFolderActivity.java b/pkg/android/phoenix/src/com/retroarch/browser/mainmenu/MigrateRetroarchFolderActivity.java
new file mode 100644
index 0000000000..1916aaa582
--- /dev/null
+++ b/pkg/android/phoenix/src/com/retroarch/browser/mainmenu/MigrateRetroarchFolderActivity.java
@@ -0,0 +1,266 @@
+package com.retroarch.browser.mainmenu;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.preference.PreferenceManager;
+import android.provider.DocumentsContract;
+import android.util.Log;
+import android.util.Pair;
+
+import com.retroarch.R;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+
+@TargetApi(26)
+public class MigrateRetroarchFolderActivity extends Activity
+{
+ final int REQUEST_CODE_GET_OLD_RETROARCH_FOLDER = 125;
+
+ @Override
+ public void onStart()
+ {
+ super.onStart();
+
+ // Needs v26 for some of the file handling functions below.
+ // Remove the TargetApi annotation to see which.
+ // If we don't have it, then just skip migration.
+ if (android.os.Build.VERSION.SDK_INT < 26) {
+ finish();
+ }
+ if(true || needToMigrate()){
+ askToMigrate();
+ }else{
+ finish();
+ }
+ }
+
+ boolean needToMigrate()
+ {
+ // As the RetroArch folder has been moved from shared storage to app-specific storage,
+ // people upgrading from older versions using the old location will need to migrate their data.
+ // We identify these users by checking that the app has been updated from an older version,
+ // and that the older version did not use the new location.
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean isNewInstall;
+ try{
+ PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0);
+ isNewInstall = info.firstInstallTime == info.lastUpdateTime;
+ }catch(PackageManager.NameNotFoundException ex) {
+ isNewInstall = true;
+ }
+
+ // Avoid asking if new install
+ if(isNewInstall && !prefs.contains("external_retroarch_folder_needs_migrate")){
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean("external_retroarch_folder_needs_migrate", false);
+ editor.apply();
+ }
+
+ return prefs.getBoolean("external_retroarch_folder_needs_migrate", true);
+ }
+
+ void askToMigrate()
+ {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.migrate_retroarch_folder_dialog_title);
+ builder.setMessage(R.string.migrate_retroarch_folder_dialog_message);
+ builder.setNegativeButton(R.string.migrate_retroarch_folder_dialog_negative, new DialogInterface.OnClickListener() {
+ @Override public void onClick(DialogInterface dialogInterface, int i) {
+ SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(MigrateRetroarchFolderActivity.this).edit();
+ editor.putBoolean("external_retroarch_folder_needs_migrate", false);
+ editor.apply();
+ MigrateRetroarchFolderActivity.this.finish();
+ }
+ });
+ builder.setNeutralButton(R.string.migrate_retroarch_folder_dialog_neutral, new DialogInterface.OnClickListener() {
+ @Override public void onClick(DialogInterface dialogInterface, int i) {
+ SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(MigrateRetroarchFolderActivity.this).edit();
+ editor.putBoolean("external_retroarch_folder_needs_migrate", true);
+ editor.apply();
+ MigrateRetroarchFolderActivity.this.finish();
+ }
+ });
+ builder.setPositiveButton(R.string.migrate_retroarch_folder_dialog_positive, new DialogInterface.OnClickListener() {
+ @Override public void onClick(DialogInterface dialogInterface, int i) {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.fromFile(new File(
+ Environment.getExternalStorageDirectory().getAbsolutePath() + "/RetroArch"
+ )));
+ startActivityForResult(intent, REQUEST_CODE_GET_OLD_RETROARCH_FOLDER);
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, Intent resultData)
+ {
+ super.onActivityResult(requestCode, resultCode, resultData);
+ if(requestCode == REQUEST_CODE_GET_OLD_RETROARCH_FOLDER){
+ if(resultCode == Activity.RESULT_OK && resultData != null){
+ copyFiles(resultData.getData());
+ }else{
+ //User cancelled or otherwise failed. Go back to the picker screen.
+ askToMigrate();
+ }
+ }
+ }
+
+ void copyFiles(Uri sourceDir)
+ {
+ final ProgressDialog pd = new ProgressDialog(this);
+ pd.setMax(100);
+ pd.setTitle(R.string.migrate_retroarch_folder_inprogress);
+ pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ pd.setCancelable(false);
+
+ CopyThread thread = new CopyThread()
+ {
+ @Override
+ protected void onPreExecute(){
+ super.onPreExecute();
+ pd.show();
+ }
+ @Override
+ protected void onProgressUpdate(Pair... params)
+ {
+ super.onProgressUpdate(params);
+ pd.setProgress(params[0].first);
+ pd.setMessage(params[0].second);
+ }
+ @Override
+ protected void onPostExecute(Boolean ok)
+ {
+ super.onPostExecute(ok);
+ pd.dismiss();
+ postMigrate(ok);
+ }
+ };
+
+ thread.execute(sourceDir);
+ }
+
+ void postMigrate(boolean ok)
+ {
+ SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
+ editor.putBoolean("external_retroarch_folder_needs_migrate", false);
+ editor.commit();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(ok ?
+ R.string.migrate_retroarch_folder_confirm :
+ R.string.migrate_retroarch_folder_confirm_witherror
+ );
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override public void onClick(DialogInterface dialogInterface, int i) {
+ MigrateRetroarchFolderActivity.this.finish();
+ }
+ });
+ builder.create().show();
+ }
+
+ class CopyThread extends AsyncTask, Boolean>
+ {
+ String PACKAGE_NAME;
+ ContentResolver resolver;
+ Uri sourceRoot;
+ boolean error;
+ ArrayList progress;
+ public CopyThread()
+ {
+ PACKAGE_NAME = MigrateRetroarchFolderActivity.this.getPackageName();
+ resolver = MigrateRetroarchFolderActivity.this.getContentResolver();
+ }
+ @Override
+ protected Boolean doInBackground(Uri... params)
+ {
+ sourceRoot = params[0];
+ error = false;
+ progress = new ArrayList<>();
+
+ String destination = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + PACKAGE_NAME + "/files/RetroArch";
+ copyFolder(sourceRoot, new File(destination));
+ return !error;
+ }
+ void copyFolder(Uri sourceUri, File dest)
+ {
+ //create destination folder
+ if(!(dest.isDirectory() || dest.mkdirs())) {
+ Log.e("MigrateRetroarchFolder", "Couldn't make new destination folder " + dest.getPath());
+ error = true;
+ return;
+ }
+
+ Uri sourceChildrenResolver;
+ try{ //for subfolders
+ sourceChildrenResolver = DocumentsContract.buildChildDocumentsUriUsingTree(sourceUri, DocumentsContract.getDocumentId(sourceUri));
+ }catch(IllegalArgumentException ex){ //for root selected by document picker
+ sourceChildrenResolver = DocumentsContract.buildChildDocumentsUriUsingTree(sourceUri, DocumentsContract.getTreeDocumentId(sourceUri));
+ }
+ progress.add(new int[]{0, 1});
+ try(
+ Cursor c = resolver.query(sourceChildrenResolver, new String[]{DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE}, null, null, null)
+ ) {
+ if(c == null) {
+ Log.e("MigrateRetroarchFolder", "Could not list files in source folder " + sourceUri.toString());
+ error = true;
+ return;
+ }
+ progress.get(progress.size() - 1)[1] = c.getCount();
+ while(c.moveToNext()){ //loop through children returned
+ String childFilename = c.getString(1);
+ Uri childUri = DocumentsContract.buildDocumentUriUsingTree(sourceUri, c.getString(0));
+ String childDocumentId = DocumentsContract.getDocumentId(childUri);
+ File destFile = new File(dest, childFilename);
+
+ if(c.getString(2).equals(DocumentsContract.Document.MIME_TYPE_DIR)){ //is a folder, recurse
+ copyFolder(childUri, destFile);
+ }else{ //is a file, copy it
+ try(
+ ParcelFileDescriptor pfd = resolver.openFileDescriptor(childUri, "r");
+ ParcelFileDescriptor.AutoCloseInputStream sourceStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+ ) {
+ Files.copy(sourceStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }catch(Exception ex){
+ Log.e("MigrateRetroarchFolder", "Error copying file " + childDocumentId, ex);
+ error = true;
+ }
+ }
+ progress.get(progress.size() - 1)[0]++;
+ publishProgress(new Pair(getProgressPercentage(), destFile.getPath()));
+ }
+ }catch(Exception ex){
+ Log.e("MigrateRetroarchFolder", "Error while copying", ex);
+ error = true;
+ }
+ progress.remove(progress.size() - 1);
+ }
+ int getProgressPercentage()
+ {
+ float sum = 0;
+ int lastDenominator = 1;
+ for(int[] frac : progress){
+ sum += ((float) frac[0]) / frac[1] / lastDenominator;
+ lastDenominator *= frac[1];
+ }
+ return (int) (sum * 100);
+ }
+ }
+}