mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-02-10 14:39:01 +01:00
Merge pull request #9221 from JosJuice/android-saf-sd-card
Android: Use storage access framework for custom SD card paths
This commit is contained in:
commit
75899b0e11
@ -35,6 +35,7 @@ import org.dolphinemu.dolphinemu.R;
|
|||||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
|
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting;
|
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting;
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
|
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
|
||||||
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
|
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
|
||||||
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity;
|
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity;
|
||||||
import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile;
|
import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile;
|
||||||
@ -169,13 +170,38 @@ public final class EmulationActivity extends AppCompatActivity
|
|||||||
if (sIgnoreLaunchRequests)
|
if (sIgnoreLaunchRequests)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
new AfterDirectoryInitializationRunner().run(activity, true, () ->
|
||||||
|
{
|
||||||
|
if (FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DEFAULT_ISO) &&
|
||||||
|
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_FS_PATH) &&
|
||||||
|
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DUMP_PATH) &&
|
||||||
|
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_LOAD_PATH) &&
|
||||||
|
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_RESOURCEPACK_PATH) &&
|
||||||
|
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_SD_PATH))
|
||||||
|
{
|
||||||
|
launchWithoutChecks(activity, filePaths);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity, R.style.DolphinDialogBase);
|
||||||
|
builder.setMessage(R.string.unavailable_paths);
|
||||||
|
builder.setPositiveButton(R.string.yes, (dialogInterface, i) ->
|
||||||
|
SettingsActivity.launch(activity, MenuTag.CONFIG_PATHS));
|
||||||
|
builder.setNeutralButton(R.string.continue_anyway, (dialogInterface, i) ->
|
||||||
|
launchWithoutChecks(activity, filePaths));
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void launchWithoutChecks(FragmentActivity activity, String[] filePaths)
|
||||||
|
{
|
||||||
sIgnoreLaunchRequests = true;
|
sIgnoreLaunchRequests = true;
|
||||||
|
|
||||||
Intent launcher = new Intent(activity, EmulationActivity.class);
|
Intent launcher = new Intent(activity, EmulationActivity.class);
|
||||||
launcher.putExtra(EXTRA_SELECTED_GAMES, filePaths);
|
launcher.putExtra(EXTRA_SELECTED_GAMES, filePaths);
|
||||||
|
|
||||||
new AfterDirectoryInitializationRunner().run(activity, true,
|
activity.startActivity(launcher);
|
||||||
() -> activity.startActivity(launcher));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void stopIgnoringLaunchRequests()
|
public static void stopIgnoringLaunchRequests()
|
||||||
|
@ -3,6 +3,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui;
|
|||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@ -18,6 +19,7 @@ import androidx.lifecycle.ViewModelProvider;
|
|||||||
|
|
||||||
import org.dolphinemu.dolphinemu.R;
|
import org.dolphinemu.dolphinemu.R;
|
||||||
import org.dolphinemu.dolphinemu.ui.main.MainActivity;
|
import org.dolphinemu.dolphinemu.ui.main.MainActivity;
|
||||||
|
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
|
||||||
import org.dolphinemu.dolphinemu.ui.main.TvMainActivity;
|
import org.dolphinemu.dolphinemu.ui.main.TvMainActivity;
|
||||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
||||||
import org.dolphinemu.dolphinemu.utils.TvUtil;
|
import org.dolphinemu.dolphinemu.utils.TvUtil;
|
||||||
@ -170,11 +172,33 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
|
|||||||
// If the user picked a file, as opposed to just backing out.
|
// If the user picked a file, as opposed to just backing out.
|
||||||
if (resultCode == MainActivity.RESULT_OK)
|
if (resultCode == MainActivity.RESULT_OK)
|
||||||
{
|
{
|
||||||
String path = FileBrowserHelper.getSelectedPath(result);
|
if (requestCode == MainPresenter.REQUEST_SD_FILE)
|
||||||
getFragment().getAdapter().onFilePickerConfirmation(path);
|
{
|
||||||
|
Uri uri = canonicalizeIfPossible(result.getData());
|
||||||
|
int takeFlags = result.getFlags() &
|
||||||
|
(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
|
||||||
|
FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.RAW_EXTENSION, () ->
|
||||||
|
{
|
||||||
|
getContentResolver().takePersistableUriPermission(uri, takeFlags);
|
||||||
|
getFragment().getAdapter().onFilePickerConfirmation(uri.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
String path = FileBrowserHelper.getSelectedPath(result);
|
||||||
|
getFragment().getAdapter().onFilePickerConfirmation(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Uri canonicalizeIfPossible(@NonNull Uri uri)
|
||||||
|
{
|
||||||
|
Uri canonicalizedUri = getContentResolver().canonicalize(uri);
|
||||||
|
return canonicalizedUri != null ? canonicalizedUri : uri;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showLoading()
|
public void showLoading()
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,9 @@ package org.dolphinemu.dolphinemu.features.settings.ui;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -289,33 +292,42 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFilePickerDirectoryClick(SettingsItem item)
|
public void onFilePickerDirectoryClick(SettingsItem item, int position)
|
||||||
{
|
{
|
||||||
mClickedItem = item;
|
mClickedItem = item;
|
||||||
|
mClickedPosition = position;
|
||||||
|
|
||||||
FileBrowserHelper.openDirectoryPicker(mView.getActivity(), FileBrowserHelper.GAME_EXTENSIONS);
|
FileBrowserHelper.openDirectoryPicker(mView.getActivity(), FileBrowserHelper.GAME_EXTENSIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFilePickerFileClick(SettingsItem item)
|
public void onFilePickerFileClick(SettingsItem item, int position)
|
||||||
{
|
{
|
||||||
mClickedItem = item;
|
mClickedItem = item;
|
||||||
|
mClickedPosition = position;
|
||||||
FilePicker filePicker = (FilePicker) item;
|
FilePicker filePicker = (FilePicker) item;
|
||||||
|
|
||||||
HashSet<String> extensions;
|
|
||||||
switch (filePicker.getRequestType())
|
switch (filePicker.getRequestType())
|
||||||
{
|
{
|
||||||
case MainPresenter.REQUEST_SD_FILE:
|
case MainPresenter.REQUEST_SD_FILE:
|
||||||
extensions = FileBrowserHelper.RAW_EXTENSION;
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("*/*");
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
{
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
|
||||||
|
filePicker.getSelectedValue(mView.getSettings()));
|
||||||
|
}
|
||||||
|
|
||||||
|
mView.getActivity().startActivityForResult(intent, filePicker.getRequestType());
|
||||||
break;
|
break;
|
||||||
case MainPresenter.REQUEST_GAME_FILE:
|
case MainPresenter.REQUEST_GAME_FILE:
|
||||||
extensions = FileBrowserHelper.GAME_EXTENSIONS;
|
FileBrowserHelper.openFilePicker(mView.getActivity(), filePicker.getRequestType(), false,
|
||||||
|
FileBrowserHelper.GAME_EXTENSIONS);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new InvalidParameterException("Unhandled request code");
|
throw new InvalidParameterException("Unhandled request code");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileBrowserHelper.openFilePicker(mView.getActivity(), filePicker.getRequestType(), false,
|
|
||||||
extensions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFilePickerConfirmation(String selectedFile)
|
public void onFilePickerConfirmation(String selectedFile)
|
||||||
@ -323,7 +335,10 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
FilePicker filePicker = (FilePicker) mClickedItem;
|
FilePicker filePicker = (FilePicker) mClickedItem;
|
||||||
|
|
||||||
if (!filePicker.getSelectedValue(mView.getSettings()).equals(selectedFile))
|
if (!filePicker.getSelectedValue(mView.getSettings()).equals(selectedFile))
|
||||||
|
{
|
||||||
|
notifyItemChanged(mClickedPosition);
|
||||||
mView.onSettingChanged();
|
mView.onSettingChanged();
|
||||||
|
}
|
||||||
|
|
||||||
filePicker.setSelectedValue(mView.getSettings(), selectedFile);
|
filePicker.setSelectedValue(mView.getSettings(), selectedFile);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder;
|
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@ -12,6 +13,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem;
|
|||||||
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter;
|
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter;
|
||||||
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
|
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
|
||||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
|
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
||||||
|
|
||||||
public final class FilePickerViewHolder extends SettingViewHolder
|
public final class FilePickerViewHolder extends SettingViewHolder
|
||||||
{
|
{
|
||||||
@ -21,6 +23,8 @@ public final class FilePickerViewHolder extends SettingViewHolder
|
|||||||
private TextView mTextSettingName;
|
private TextView mTextSettingName;
|
||||||
private TextView mTextSettingDescription;
|
private TextView mTextSettingDescription;
|
||||||
|
|
||||||
|
private Drawable mDefaultBackground;
|
||||||
|
|
||||||
public FilePickerViewHolder(View itemView, SettingsAdapter adapter)
|
public FilePickerViewHolder(View itemView, SettingsAdapter adapter)
|
||||||
{
|
{
|
||||||
super(itemView, adapter);
|
super(itemView, adapter);
|
||||||
@ -31,6 +35,8 @@ public final class FilePickerViewHolder extends SettingViewHolder
|
|||||||
{
|
{
|
||||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||||
|
|
||||||
|
mDefaultBackground = root.getBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -39,6 +45,17 @@ public final class FilePickerViewHolder extends SettingViewHolder
|
|||||||
mFilePicker = (FilePicker) item;
|
mFilePicker = (FilePicker) item;
|
||||||
mItem = item;
|
mItem = item;
|
||||||
|
|
||||||
|
String path = mFilePicker.getSelectedValue(getAdapter().getSettings());
|
||||||
|
|
||||||
|
if (FileBrowserHelper.isPathEmptyOrValid(path))
|
||||||
|
{
|
||||||
|
itemView.setBackground(mDefaultBackground);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
itemView.setBackgroundResource(R.drawable.invalid_setting_background);
|
||||||
|
}
|
||||||
|
|
||||||
mTextSettingName.setText(item.getNameId());
|
mTextSettingName.setText(item.getNameId());
|
||||||
|
|
||||||
if (item.getDescriptionId() > 0)
|
if (item.getDescriptionId() > 0)
|
||||||
@ -47,8 +64,6 @@ public final class FilePickerViewHolder extends SettingViewHolder
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
String path = mFilePicker.getSelectedValue(getAdapter().getSettings());
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(path))
|
if (TextUtils.isEmpty(path))
|
||||||
{
|
{
|
||||||
String defaultPathRelative = mFilePicker.getDefaultPathRelativeToUserDirectory();
|
String defaultPathRelative = mFilePicker.getDefaultPathRelativeToUserDirectory();
|
||||||
@ -73,13 +88,14 @@ public final class FilePickerViewHolder extends SettingViewHolder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int position = getAdapterPosition();
|
||||||
if (mFilePicker.getRequestType() == MainPresenter.REQUEST_DIRECTORY)
|
if (mFilePicker.getRequestType() == MainPresenter.REQUEST_DIRECTORY)
|
||||||
{
|
{
|
||||||
getAdapter().onFilePickerDirectoryClick(mItem);
|
getAdapter().onFilePickerDirectoryClick(mItem, position);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
getAdapter().onFilePickerFileClick(mItem);
|
getAdapter().onFilePickerFileClick(mItem, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyle(mTextSettingName, mItem);
|
setStyle(mTextSettingName, mItem);
|
||||||
|
@ -205,7 +205,9 @@ public final class MainActivity extends AppCompatActivity implements MainView
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MainPresenter.REQUEST_WAD_FILE:
|
case MainPresenter.REQUEST_WAD_FILE:
|
||||||
mPresenter.installWAD(result.getData().toString());
|
FileBrowserHelper.runAfterExtensionCheck(this, result.getData(),
|
||||||
|
FileBrowserHelper.WAD_EXTENSION,
|
||||||
|
() -> mPresenter.installWAD(result.getData().toString()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,9 @@ public final class TvMainActivity extends FragmentActivity implements MainView
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MainPresenter.REQUEST_WAD_FILE:
|
case MainPresenter.REQUEST_WAD_FILE:
|
||||||
mPresenter.installWAD(result.getData().toString());
|
FileBrowserHelper.runAfterExtensionCheck(this, result.getData(),
|
||||||
|
FileBrowserHelper.WAD_EXTENSION,
|
||||||
|
() -> mPresenter.installWAD(result.getData().toString()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
package org.dolphinemu.dolphinemu.utils;
|
package org.dolphinemu.dolphinemu.utils;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
|
import android.provider.DocumentsContract.Document;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
import androidx.annotation.Keep;
|
||||||
|
|
||||||
@ -17,10 +22,16 @@ public class ContentHandler
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return DolphinApplication.getAppContext().getContentResolver()
|
return getContentResolver().openFileDescriptor(Uri.parse(uri), mode).detachFd();
|
||||||
.openFileDescriptor(Uri.parse(uri), mode).detachFd();
|
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException | NullPointerException e)
|
catch (SecurityException e)
|
||||||
|
{
|
||||||
|
Log.error("Tried to open " + uri + " without permission");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Some content providers throw IllegalArgumentException for invalid modes,
|
||||||
|
// despite the documentation saying that invalid modes result in a FileNotFoundException
|
||||||
|
catch (FileNotFoundException | IllegalArgumentException | NullPointerException e)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -31,8 +42,12 @@ public class ContentHandler
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ContentResolver resolver = DolphinApplication.getAppContext().getContentResolver();
|
return DocumentsContract.deleteDocument(getContentResolver(), Uri.parse(uri));
|
||||||
return DocumentsContract.deleteDocument(resolver, Uri.parse(uri));
|
}
|
||||||
|
catch (SecurityException e)
|
||||||
|
{
|
||||||
|
Log.error("Tried to delete " + uri + " without permission");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException e)
|
catch (FileNotFoundException e)
|
||||||
{
|
{
|
||||||
@ -40,4 +55,46 @@ public class ContentHandler
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean exists(@NonNull String uri)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE};
|
||||||
|
try (Cursor cursor = getContentResolver().query(Uri.parse(uri), projection, null, null, null))
|
||||||
|
{
|
||||||
|
return cursor != null && cursor.getCount() > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (SecurityException e)
|
||||||
|
{
|
||||||
|
Log.error("Tried to check if " + uri + " exists without permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String getDisplayName(@NonNull Uri uri)
|
||||||
|
{
|
||||||
|
final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME};
|
||||||
|
try (Cursor cursor = getContentResolver().query(uri, projection, null, null, null))
|
||||||
|
{
|
||||||
|
if (cursor != null && cursor.moveToFirst())
|
||||||
|
{
|
||||||
|
return cursor.getString(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (SecurityException e)
|
||||||
|
{
|
||||||
|
Log.error("Tried to get display name of " + uri + " without permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContentResolver getContentResolver()
|
||||||
|
{
|
||||||
|
return DolphinApplication.getAppContext().getContentResolver();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
package org.dolphinemu.dolphinemu.utils;
|
package org.dolphinemu.dolphinemu.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import com.nononsenseapps.filepicker.FilePickerActivity;
|
import com.nononsenseapps.filepicker.FilePickerActivity;
|
||||||
import com.nononsenseapps.filepicker.Utils;
|
import com.nononsenseapps.filepicker.Utils;
|
||||||
|
|
||||||
|
import org.dolphinemu.dolphinemu.R;
|
||||||
import org.dolphinemu.dolphinemu.activities.CustomFilePickerActivity;
|
import org.dolphinemu.dolphinemu.activities.CustomFilePickerActivity;
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
|
||||||
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
|
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public final class FileBrowserHelper
|
public final class FileBrowserHelper
|
||||||
{
|
{
|
||||||
@ -27,6 +33,9 @@ public final class FileBrowserHelper
|
|||||||
public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
|
public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
|
||||||
"raw"));
|
"raw"));
|
||||||
|
|
||||||
|
public static final HashSet<String> WAD_EXTENSION = new HashSet<>(Collections.singletonList(
|
||||||
|
"wad"));
|
||||||
|
|
||||||
public static void openDirectoryPicker(FragmentActivity activity, HashSet<String> extensions)
|
public static void openDirectoryPicker(FragmentActivity activity, HashSet<String> extensions)
|
||||||
{
|
{
|
||||||
Intent i = new Intent(activity, CustomFilePickerActivity.class);
|
Intent i = new Intent(activity, CustomFilePickerActivity.class);
|
||||||
@ -85,4 +94,83 @@ public final class FileBrowserHelper
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isPathEmptyOrValid(StringSetting path)
|
||||||
|
{
|
||||||
|
return isPathEmptyOrValid(path.getStringGlobal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPathEmptyOrValid(String path)
|
||||||
|
{
|
||||||
|
return !path.startsWith("content://") || ContentHandler.exists(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void runAfterExtensionCheck(Context context, Uri uri, Set<String> validExtensions,
|
||||||
|
Runnable runnable)
|
||||||
|
{
|
||||||
|
String extension = null;
|
||||||
|
|
||||||
|
String path = uri.getLastPathSegment();
|
||||||
|
if (path != null)
|
||||||
|
extension = getExtension(new File(path).getName());
|
||||||
|
|
||||||
|
if (extension == null)
|
||||||
|
extension = getExtension(ContentHandler.getDisplayName(uri));
|
||||||
|
|
||||||
|
if (extension != null && validExtensions.contains(extension))
|
||||||
|
{
|
||||||
|
runnable.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String message;
|
||||||
|
if (extension == null)
|
||||||
|
{
|
||||||
|
message = context.getString(R.string.no_file_extension);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int messageId = validExtensions.size() == 1 ?
|
||||||
|
R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple;
|
||||||
|
|
||||||
|
ArrayList<String> extensionsList = new ArrayList<>(validExtensions);
|
||||||
|
Collections.sort(extensionsList);
|
||||||
|
|
||||||
|
message = context.getString(messageId, extension, join(", ", extensionsList));
|
||||||
|
}
|
||||||
|
|
||||||
|
new AlertDialog.Builder(context, R.style.DolphinDialogBase)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(R.string.yes, (dialogInterface, i) -> runnable.run())
|
||||||
|
.setNegativeButton(R.string.no, null)
|
||||||
|
.setCancelable(false)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String getExtension(@Nullable String fileName)
|
||||||
|
{
|
||||||
|
if (fileName == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int dotIndex = fileName.lastIndexOf(".");
|
||||||
|
return dotIndex != -1 ? fileName.substring(dotIndex + 1) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this with String.join once we can use Java 8
|
||||||
|
private static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
boolean first = true;
|
||||||
|
for (CharSequence element : elements)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
sb.append(delimiter);
|
||||||
|
first = false;
|
||||||
|
sb.append(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:attr/selectableItemBackground"/>
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/invalid_setting_overlay" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
@ -11,4 +11,6 @@
|
|||||||
|
|
||||||
<color name="tv_card_unselected">#444444</color>
|
<color name="tv_card_unselected">#444444</color>
|
||||||
|
|
||||||
|
<color name="invalid_setting_overlay">#36ff0000</color>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -315,6 +315,7 @@
|
|||||||
<string name="clear">Clear</string>
|
<string name="clear">Clear</string>
|
||||||
<string name="disabled">Disabled</string>
|
<string name="disabled">Disabled</string>
|
||||||
<string name="other">Other</string>
|
<string name="other">Other</string>
|
||||||
|
<string name="continue_anyway">Continue Anyway</string>
|
||||||
|
|
||||||
<!-- Game Grid Screen-->
|
<!-- Game Grid Screen-->
|
||||||
<string name="add_directory_title">Add Folder to Library</string>
|
<string name="add_directory_title">Add Folder to Library</string>
|
||||||
@ -433,6 +434,12 @@ It can efficiently compress both junk data and encrypted Wii data.
|
|||||||
|
|
||||||
<string name="select_dir">Select This Directory</string>
|
<string name="select_dir">Select This Directory</string>
|
||||||
|
|
||||||
|
<!-- File Pickers -->
|
||||||
|
<string name="no_file_extension">The selected file does not appear to have a file name extension.\n\nContinue anyway?</string>
|
||||||
|
<string name="wrong_file_extension_single">The selected file has the file name extension \"%1$s\", but \"%2$s\" was expected.\n\nContinue anyway?</string>
|
||||||
|
<string name="wrong_file_extension_multiple">The selected file has the file name extension \"%1$s\", but one of these extensions was expected: %2$s\n\nContinue anyway?</string>
|
||||||
|
<string name="unavailable_paths">Dolphin does not have permission to access one or more configured paths. Would you like to fix this before starting?</string>
|
||||||
|
|
||||||
<!-- Misc -->
|
<!-- Misc -->
|
||||||
<string name="pitch">Total Pitch</string>
|
<string name="pitch">Total Pitch</string>
|
||||||
<string name="yaw">Total Yaw</string>
|
<string name="yaw">Total Yaw</string>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "Common/Assert.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
#include "jni/AndroidCommon/IDCache.h"
|
#include "jni/AndroidCommon/IDCache.h"
|
||||||
|
|
||||||
@ -42,21 +43,35 @@ std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsPathAndroidContent(const std::string& uri)
|
||||||
|
{
|
||||||
|
return StringBeginsWith(uri, "content://");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OpenModeToAndroid(std::string mode)
|
||||||
|
{
|
||||||
|
// The 'b' specifier is not supported. Since we're on POSIX, it's fine to just skip it.
|
||||||
|
if (!mode.empty() && mode.back() == 'b')
|
||||||
|
mode.pop_back();
|
||||||
|
|
||||||
|
if (mode == "r+")
|
||||||
|
mode = "rw";
|
||||||
|
else if (mode == "w+")
|
||||||
|
mode = "rwt";
|
||||||
|
else if (mode == "a+")
|
||||||
|
mode = "rwa";
|
||||||
|
else if (mode == "a")
|
||||||
|
mode = "wa";
|
||||||
|
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
int OpenAndroidContent(const std::string& uri, const std::string& mode)
|
int OpenAndroidContent(const std::string& uri, const std::string& mode)
|
||||||
{
|
{
|
||||||
JNIEnv* env = IDCache::GetEnvForThread();
|
JNIEnv* env = IDCache::GetEnvForThread();
|
||||||
const jint fd = env->CallStaticIntMethod(IDCache::GetContentHandlerClass(),
|
return env->CallStaticIntMethod(IDCache::GetContentHandlerClass(),
|
||||||
IDCache::GetContentHandlerOpenFd(), ToJString(env, uri),
|
IDCache::GetContentHandlerOpenFd(), ToJString(env, uri),
|
||||||
ToJString(env, mode));
|
ToJString(env, mode));
|
||||||
|
|
||||||
// We can get an IllegalArgumentException when passing an invalid mode
|
|
||||||
if (env->ExceptionCheck())
|
|
||||||
{
|
|
||||||
env->ExceptionDescribe();
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
return fd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeleteAndroidContent(const std::string& uri)
|
bool DeleteAndroidContent(const std::string& uri)
|
||||||
|
@ -12,7 +12,16 @@ std::string GetJString(JNIEnv* env, jstring jstr);
|
|||||||
jstring ToJString(JNIEnv* env, const std::string& str);
|
jstring ToJString(JNIEnv* env, const std::string& str);
|
||||||
std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array);
|
std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array);
|
||||||
|
|
||||||
|
// Returns true if the given path should be opened as Android content instead of a normal file.
|
||||||
|
bool IsPathAndroidContent(const std::string& uri);
|
||||||
|
|
||||||
|
// Turns a C/C++ style mode (e.g. "rb") into one which can be used with OpenAndroidContent.
|
||||||
|
std::string OpenModeToAndroid(std::string mode);
|
||||||
|
|
||||||
|
// Opens a given file and returns a file descriptor.
|
||||||
int OpenAndroidContent(const std::string& uri, const std::string& mode);
|
int OpenAndroidContent(const std::string& uri, const std::string& mode);
|
||||||
|
|
||||||
|
// Deletes a given file.
|
||||||
bool DeleteAndroidContent(const std::string& uri);
|
bool DeleteAndroidContent(const std::string& uri);
|
||||||
int GetNetworkIpAddress();
|
int GetNetworkIpAddress();
|
||||||
int GetNetworkPrefixLength();
|
int GetNetworkPrefixLength();
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include "Common/StringUtil.h"
|
|
||||||
#include "jni/AndroidCommon/AndroidCommon.h"
|
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -66,24 +65,17 @@ void IOFile::Swap(IOFile& other) noexcept
|
|||||||
bool IOFile::Open(const std::string& filename, const char openmode[])
|
bool IOFile::Open(const std::string& filename, const char openmode[])
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
m_good = _tfopen_s(&m_file, UTF8ToTStr(filename).c_str(), UTF8ToTStr(openmode).c_str()) == 0;
|
m_good = _tfopen_s(&m_file, UTF8ToTStr(filename).c_str(), UTF8ToTStr(openmode).c_str()) == 0;
|
||||||
#else
|
#else
|
||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
if (StringBeginsWith(filename, "content://"))
|
if (IsPathAndroidContent(filename))
|
||||||
{
|
m_file = fdopen(OpenAndroidContent(filename, OpenModeToAndroid(openmode)), openmode);
|
||||||
// The Java method which OpenAndroidContent passes the mode to does not support the b specifier.
|
|
||||||
// Since we're on POSIX, it's fine to just remove the b.
|
|
||||||
std::string mode_without_b(openmode);
|
|
||||||
mode_without_b.erase(std::remove(mode_without_b.begin(), mode_without_b.end(), 'b'),
|
|
||||||
mode_without_b.end());
|
|
||||||
m_file = fdopen(OpenAndroidContent(filename, mode_without_b), mode_without_b.c_str());
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
#endif
|
#endif
|
||||||
{
|
|
||||||
m_file = std::fopen(filename.c_str(), openmode);
|
m_file = std::fopen(filename.c_str(), openmode);
|
||||||
}
|
|
||||||
m_good = m_file != nullptr;
|
m_good = m_file != nullptr;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user