Added option to open the cemu folder

This commit is contained in:
SSimco
2024-09-25 18:40:37 +03:00
parent c8fe2428a0
commit 2b619cfcfb
6 changed files with 350 additions and 12 deletions
+11 -10
View File
@@ -80,23 +80,24 @@ android {
}
}
buildFeatures {
buildConfig true
dataBinding true
viewBinding true
}
}
dependencies {
implementation "androidx.datastore:datastore-rxjava3:1.1.0"
implementation "androidx.datastore:datastore-preferences:1.1.0"
implementation "androidx.datastore:datastore:1.1.0"
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation "androidx.datastore:datastore-rxjava3:1.1.1"
implementation "androidx.datastore:datastore-preferences:1.1.1"
implementation "androidx.datastore:datastore:1.1.1"
implementation "com.squareup.okhttp3:okhttp:4.12.0"
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.navigation:navigation-fragment:2.5.3'
implementation 'androidx.navigation:navigation-ui:2.5.3'
implementation 'androidx.navigation:navigation-fragment:2.8.1'
implementation 'androidx.navigation:navigation-ui:2.8.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
@@ -65,5 +65,16 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name=".features.DocumentsProvider"
android:authorities="${applicationId}.provider"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest>
@@ -3,23 +3,36 @@ package info.cemu.Cemu;
import android.app.Application;
import android.util.DisplayMetrics;
import java.io.File;
import java.util.Objects;
import info.cemu.Cemu.NativeLibrary;
import info.cemu.Cemu.utils.FileUtil;
public class CemuApplication extends Application {
private static CemuApplication application;
public CemuApplication() {
application = this;
}
public static CemuApplication getApplication() {
return application;
}
public File getInternalFolder() {
var externalFilesDir = getExternalFilesDir(null);
if (externalFilesDir != null)
return externalFilesDir;
return getFilesDir();
}
@Override
public void onCreate() {
super.onCreate();
application = this;
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
NativeLibrary.setDPI(displayMetrics.density);
NativeLibrary.initializeActiveSettings(getExternalFilesDir(null).getAbsoluteFile().toString(), getExternalFilesDir(null).getAbsoluteFile().toString());
NativeLibrary.initializeActiveSettings(getInternalFolder().toString(), getInternalFolder().toString());
NativeLibrary.initializeEmulation();
}
}
@@ -0,0 +1,293 @@
// Based on:
// Skyline
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
package info.cemu.Cemu.features;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Objects;
import info.cemu.Cemu.CemuApplication;
import info.cemu.Cemu.BuildConfig;
import info.cemu.Cemu.R;
public class DocumentsProvider extends android.provider.DocumentsProvider {
private final File baseDirectory;
private final String applicationName = CemuApplication.getApplication().getApplicationInfo().loadLabel(CemuApplication.getApplication().getPackageManager()).toString();
public DocumentsProvider() {
try {
baseDirectory = CemuApplication.getApplication().getInternalFolder().getCanonicalFile();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private final String[] DEFAULT_ROOT_PROJECTION = {
DocumentsContract.Root.COLUMN_ROOT_ID,
DocumentsContract.Root.COLUMN_MIME_TYPES,
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.COLUMN_ICON,
DocumentsContract.Root.COLUMN_TITLE,
DocumentsContract.Root.COLUMN_SUMMARY,
DocumentsContract.Root.COLUMN_DOCUMENT_ID,
DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
};
private final String[] DEFAULT_DOCUMENT_PROJECTION = {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_FLAGS,
DocumentsContract.Document.COLUMN_SIZE
};
public static final String ROOT_ID = "root";
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".provider";
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor queryRoots(String[] projection) {
var cursor = new MatrixCursor(projection == null ? DEFAULT_ROOT_PROJECTION : projection);
cursor.newRow().add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
.add(DocumentsContract.Root.COLUMN_SUMMARY, null)
.add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE | DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD)
.add(DocumentsContract.Root.COLUMN_TITLE, applicationName)
.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory))
.add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*")
.add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.getFreeSpace())
.add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher);
return cursor;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
var cursor = new MatrixCursor(projection == null ? DEFAULT_DOCUMENT_PROJECTION : projection);
includeFile(cursor, documentId, null);
return cursor;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
if (parentDocumentId == null || documentId == null) return false;
return documentId.startsWith(parentDocumentId);
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
var parentFile = getFile(parentDocumentId);
var newFile = resolveWithoutConflict(parentFile, displayName);
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
if (!newFile.mkdir())
throw new FileNotFoundException("Failed to create directory");
} else {
try {
if (!newFile.createNewFile())
throw new FileNotFoundException("Failed to create file");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return getDocumentId(newFile);
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
var file = getFile(documentId);
if (!file.delete())
throw new FileNotFoundException("Couldn't delete document with ID '$documentId'");
}
@Override
public void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
var parent = getFile(parentDocumentId);
var file = getFile(documentId);
if (parent.equals(file) || file.getParentFile() == null || file.getParentFile().equals(parent)) {
if (!file.delete())
throw new FileNotFoundException("Couldn't delete document with ID '$documentId'");
} else {
throw new FileNotFoundException("Couldn't delete document with ID '$documentId'");
}
}
@Override
public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
if (displayName == null)
throw new FileNotFoundException("Couldn't rename document '$documentId' as the new name is null");
var sourceFile = getFile(documentId);
var sourceParentFile = sourceFile.getParentFile();
if (sourceParentFile == null)
throw new FileNotFoundException("Couldn't rename document '$documentId' as it has no parent");
var destFile = resolve(sourceParentFile, displayName);
try {
if (!sourceFile.renameTo(destFile))
throw new FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'");
} catch (Exception exception) {
throw new FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}");
}
return getDocumentId(destFile);
}
@Override
public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException {
var parent = getFile(targetParentDocumentId);
var oldFile = getFile(sourceDocumentId);
var newFile = resolveWithoutConflict(parent, oldFile.getName());
try {
if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true)))
throw new IOException("Couldn't create new file");
try (var inputStream = new FileInputStream(oldFile); var outputStream = new FileOutputStream(newFile)) {
byte[] b = new byte[1024];
int len;
while ((len = inputStream.read(b, 0, 1024)) > 0) {
outputStream.write(b, 0, len);
}
}
} catch (IOException exception) {
throw new FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}");
}
return getDocumentId(newFile);
}
@Override
public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
try {
var newDocumentId = copyDocument(sourceDocumentId, sourceParentDocumentId, targetParentDocumentId);
removeDocument(sourceDocumentId, sourceParentDocumentId);
return newDocumentId;
} catch (FileNotFoundException e) {
throw new FileNotFoundException("Couldn't move document '$sourceDocumentId'");
}
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
var cursor = new MatrixCursor(projection == null ? DEFAULT_DOCUMENT_PROJECTION : projection);
var parent = getFile(parentDocumentId);
var files = parent.listFiles();
if (files == null)
return cursor;
for (var file : files)
includeFile(cursor, null, file);
return cursor;
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
var file = getFile(documentId);
var accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
private File resolve(File file, String other) {
return file.toPath().resolve(other).toFile();
}
private String copyDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
if (!isChildDocument(sourceParentDocumentId, sourceDocumentId))
throw new FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'");
return copyDocument(sourceDocumentId, targetParentDocumentId);
}
private File resolveWithoutConflict(File originalFile, String name) {
var file = resolve(originalFile, name);
if (!file.exists())
return file;
// Makes sure two files don't have the same name by adding a number to the end
var noConflictId = 1;
var periodIndex = name.lastIndexOf('.');
var extension = "";
var baseName = name;
if (periodIndex != -1) {
baseName = name.substring(0, periodIndex);
extension = name.substring(periodIndex);
}
while (file.exists()) {
String newFileName = baseName + " (" + noConflictId + ")" + extension;
file = file.toPath().resolve(newFileName).toFile();
}
return file;
}
private void includeFile(MatrixCursor cursor, String documentId, File file) throws FileNotFoundException {
var localDocumentId = documentId == null ? getDocumentId(file) : documentId;
var localFile = file == null ? getFile(documentId) : file;
int flags = 0;
if (localFile.isDirectory() && localFile.canWrite()) {
flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (localFile.canWrite()) {
flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE
| DocumentsContract.Document.FLAG_SUPPORTS_MOVE
| DocumentsContract.Document.FLAG_SUPPORTS_COPY
| DocumentsContract.Document.FLAG_SUPPORTS_RENAME;
}
flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_DELETE
| DocumentsContract.Document.FLAG_SUPPORTS_REMOVE;
var curorRowBuilder = cursor.newRow().add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId)
.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, localFile.equals(baseDirectory) ? applicationName : localFile.getName())
.add(DocumentsContract.Document.COLUMN_SIZE, localFile.length())
.add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile))
.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified())
.add(DocumentsContract.Document.COLUMN_FLAGS, flags);
if (localFile.equals(baseDirectory))
curorRowBuilder.add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher);
}
private String getTypeForFile(File file) {
if (file.isDirectory())
return DocumentsContract.Document.MIME_TYPE_DIR;
return getTypeForName(file.getName());
}
private String getTypeForName(String name) {
var lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
var extension = name.substring(lastDot + 1);
var mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null)
return mime;
}
return "application/octect-stream";
}
private File getFile(String documentId) throws FileNotFoundException {
Objects.requireNonNull(documentId);
if (documentId.startsWith(ROOT_ID)) {
var file = resolve(baseDirectory, documentId.substring(ROOT_ID.length() + 1));
if (!file.exists())
throw new FileNotFoundException(file.getAbsolutePath() + " " + documentId + " not found");
return file;
} else {
throw new FileNotFoundException(documentId + " is not in any known root");
}
}
private String getDocumentId(File file) {
return ROOT_ID + "/" + baseDirectory.toPath().relativize(file.toPath()).toString();
}
}
@@ -1,9 +1,13 @@
package info.cemu.Cemu.settings;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
@@ -11,6 +15,7 @@ import androidx.navigation.fragment.NavHostFragment;
import info.cemu.Cemu.R;
import info.cemu.Cemu.databinding.GenericRecyclerViewLayoutBinding;
import info.cemu.Cemu.features.DocumentsProvider;
import info.cemu.Cemu.guibasecomponents.ButtonRecyclerViewItem;
import info.cemu.Cemu.guibasecomponents.GenericRecyclerViewAdapter;
import info.cemu.Cemu.guibasecomponents.SimpleButtonRecyclerViewItem;
@@ -21,6 +26,7 @@ public class SettingsFragment extends Fragment {
GenericRecyclerViewLayoutBinding binding = GenericRecyclerViewLayoutBinding.inflate(inflater, container, false);
GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter();
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.open_cemu_folder), this::openCemuFolder));
genericRecyclerViewAdapter.addRecyclerViewItem(new ButtonRecyclerViewItem(getString(R.string.add_game_path), getString(R.string.games_folder_description), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_gamePathsFragment)));
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.input_settings), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_inputSettingsFragment)));
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.graphics_settings), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_graphicSettingsFragment)));
@@ -31,4 +37,16 @@ public class SettingsFragment extends Fragment {
return binding.getRoot();
}
private void openCemuFolder() {
try {
var intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setData(DocumentsContract.buildRootUri(DocumentsProvider.AUTHORITY, DocumentsProvider.ROOT_ID));
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
requireContext().startActivity(intent);
} catch (ActivityNotFoundException activityNotFoundException) {
Toast.makeText(requireContext(), R.string.failed_to_open_cemu_folder, Toast.LENGTH_LONG).show();
}
}
}
@@ -176,4 +176,6 @@
<string name="game_not_found">Unable to launch game because the base files were not found.</string>
<string name="failed_create_surface_error">Failed creating surface: %1$s</string>
<string name="failed_initialize_renderer_error">Failed to initialize renderer: %1$s</string>
<string name="open_cemu_folder">Open Cemu folder</string>
<string name="failed_to_open_cemu_folder">Could not open Cemu folder</string>
</resources>