mirror of
https://github.com/libretro/citra.git
synced 2024-10-06 22:23:23 +00:00
Sync android dir with master
This commit is contained in:
parent
73dcfb734d
commit
3ff4efde12
@ -1,76 +0,0 @@
|
||||
// Copyright 2019 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.DocumentsTree;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
public class CitraApplication extends Application {
|
||||
public static GameDatabase databaseHelper;
|
||||
public static DocumentsTree documentsTree;
|
||||
private static CitraApplication application;
|
||||
|
||||
private void createNotificationChannel() {
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
{
|
||||
// General notification
|
||||
CharSequence name = getString(R.string.app_notification_channel_name);
|
||||
String description = getString(R.string.app_notification_channel_description);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
getString(R.string.app_notification_channel_id), name,
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription(description);
|
||||
channel.setSound(null, null);
|
||||
channel.setVibrationPattern(null);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
{
|
||||
// CIA Install notifications
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
getString(R.string.cia_install_notification_channel_id),
|
||||
getString(R.string.cia_install_notification_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.setDescription(getString(R.string.cia_install_notification_channel_description));
|
||||
channel.setSound(null, null);
|
||||
channel.setVibrationPattern(null);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
application = this;
|
||||
documentsTree = new DocumentsTree();
|
||||
|
||||
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
|
||||
DirectoryInitialization.start(getApplicationContext());
|
||||
}
|
||||
|
||||
NativeLibrary.LogDeviceInfo();
|
||||
createNotificationChannel();
|
||||
|
||||
databaseHelper = new GameDatabase(this);
|
||||
}
|
||||
|
||||
public static Context getAppContext() {
|
||||
return application.getApplicationContext();
|
||||
}
|
||||
}
|
@ -1,720 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Dolphin Emulator Project
|
||||
* Licensed under GPLv2+
|
||||
* Refer to the license.txt file included.
|
||||
*/
|
||||
|
||||
package org.citra.citra_emu;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.Surface;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.applets.SoftwareKeyboard;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import static android.Manifest.permission.CAMERA;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
/**
|
||||
* Class which contains methods that interact
|
||||
* with the native side of the Citra code.
|
||||
*/
|
||||
public final class NativeLibrary {
|
||||
/**
|
||||
* Default touchscreen device
|
||||
*/
|
||||
public static final String TouchScreenDevice = "Touchscreen";
|
||||
public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null);
|
||||
|
||||
private static boolean alertResult = false;
|
||||
private static String alertPromptResult = "";
|
||||
private static int alertPromptButton = 0;
|
||||
private static final Object alertPromptLock = new Object();
|
||||
private static boolean alertPromptInProgress = false;
|
||||
private static String alertPromptCaption = "";
|
||||
private static int alertPromptButtonConfig = 0;
|
||||
private static EditText alertPromptEditText = null;
|
||||
|
||||
static {
|
||||
try {
|
||||
System.loadLibrary("citra-android");
|
||||
} catch (UnsatisfiedLinkError ex) {
|
||||
Log.error("[NativeLibrary] " + ex.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private NativeLibrary() {
|
||||
// Disallows instantiation.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles button press events for a gamepad.
|
||||
*
|
||||
* @param Device The input descriptor of the gamepad.
|
||||
* @param Button Key code identifying which button was pressed.
|
||||
* @param Action Mask identifying which action is happening (button pressed down, or button released).
|
||||
* @return If we handled the button press.
|
||||
*/
|
||||
public static native boolean onGamePadEvent(String Device, int Button, int Action);
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param Device The device ID of the gamepad.
|
||||
* @param Axis The axis ID
|
||||
* @param x_axis The value of the x-axis represented by the given ID.
|
||||
* @param y_axis The value of the y-axis represented by the given ID
|
||||
*/
|
||||
public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis);
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param Device The device ID of the gamepad.
|
||||
* @param Axis_id The axis ID
|
||||
* @param axis_val The value of the axis represented by the given ID.
|
||||
*/
|
||||
public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val);
|
||||
|
||||
/**
|
||||
* Handles touch events.
|
||||
*
|
||||
* @param x_axis The value of the x-axis.
|
||||
* @param y_axis The value of the y-axis
|
||||
* @param pressed To identify if the touch held down or released.
|
||||
* @return true if the pointer is within the touchscreen
|
||||
*/
|
||||
public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed);
|
||||
|
||||
/**
|
||||
* Handles touch movement.
|
||||
*
|
||||
* @param x_axis The value of the instantaneous x-axis.
|
||||
* @param y_axis The value of the instantaneous y-axis.
|
||||
*/
|
||||
public static native void onTouchMoved(float x_axis, float y_axis);
|
||||
|
||||
public static native void ReloadSettings();
|
||||
|
||||
public static native String GetUserSetting(String gameID, String Section, String Key);
|
||||
|
||||
public static native void SetUserSetting(String gameID, String Section, String Key, String Value);
|
||||
|
||||
public static native void InitGameIni(String gameID);
|
||||
|
||||
public static native long GetTitleId(String filename);
|
||||
|
||||
public static native String GetGitRevision();
|
||||
|
||||
/**
|
||||
* Sets the current working user directory
|
||||
* If not set, it auto-detects a location
|
||||
*/
|
||||
public static native void SetUserDirectory(String directory);
|
||||
|
||||
public static native String[] GetInstalledGamePaths();
|
||||
|
||||
// Create the config.ini file.
|
||||
public static native void CreateConfigFile();
|
||||
|
||||
public static native void CreateLogFile();
|
||||
|
||||
public static native void LogUserDirectory(String directory);
|
||||
|
||||
public static native int DefaultCPUCore();
|
||||
|
||||
/**
|
||||
* Begins emulation.
|
||||
*/
|
||||
public static native void Run(String path);
|
||||
|
||||
public static native String[] GetTextureFilterNames();
|
||||
|
||||
/**
|
||||
* Begins emulation from the specified savestate.
|
||||
*/
|
||||
public static native void Run(String path, String savestatePath, boolean deleteSavestate);
|
||||
|
||||
// Surface Handling
|
||||
public static native void SurfaceChanged(Surface surf);
|
||||
|
||||
public static native void SurfaceDestroyed();
|
||||
|
||||
public static native void DoFrame();
|
||||
|
||||
/**
|
||||
* Unpauses emulation from a paused state.
|
||||
*/
|
||||
public static native void UnPauseEmulation();
|
||||
|
||||
/**
|
||||
* Pauses emulation.
|
||||
*/
|
||||
public static native void PauseEmulation();
|
||||
|
||||
/**
|
||||
* Stops emulation.
|
||||
*/
|
||||
public static native void StopEmulation();
|
||||
|
||||
/**
|
||||
* Returns true if emulation is running (or is paused).
|
||||
*/
|
||||
public static native boolean IsRunning();
|
||||
|
||||
/**
|
||||
* Returns the title ID of the currently running title, or 0 on failure.
|
||||
*/
|
||||
public static native long GetRunningTitleId();
|
||||
|
||||
/**
|
||||
* Returns the performance stats for the current game
|
||||
**/
|
||||
public static native double[] GetPerfStats();
|
||||
|
||||
/**
|
||||
* Notifies the core emulation that the orientation has changed.
|
||||
*/
|
||||
public static native void NotifyOrientationChange(int layout_option, int rotation);
|
||||
|
||||
/**
|
||||
* Swaps the top and bottom screens.
|
||||
*/
|
||||
public static native void SwapScreens(boolean swap_screens, int rotation);
|
||||
|
||||
public enum CoreError {
|
||||
ErrorSystemFiles,
|
||||
ErrorSavestate,
|
||||
ErrorUnknown,
|
||||
}
|
||||
|
||||
private static boolean coreErrorAlertResult = false;
|
||||
private static final Object coreErrorAlertLock = new Object();
|
||||
|
||||
public static class CoreErrorDialogFragment extends DialogFragment {
|
||||
static CoreErrorDialogFragment newInstance(String title, String message) {
|
||||
CoreErrorDialogFragment frag = new CoreErrorDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("title", title);
|
||||
args.putString("message", message);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity emulationActivity = Objects.requireNonNull(getActivity());
|
||||
|
||||
final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
|
||||
final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
|
||||
|
||||
return new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.continue_button, (dialog, which) -> {
|
||||
coreErrorAlertResult = true;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.abort_button, (dialog, which) -> {
|
||||
coreErrorAlertResult = false;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
}).setOnDismissListener(dialog -> {
|
||||
coreErrorAlertResult = true;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
}).create();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnCoreErrorImpl(String title, String message) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return;
|
||||
}
|
||||
|
||||
CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message);
|
||||
fragment.show(emulationActivity.getSupportFragmentManager(), "coreError");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a core error.
|
||||
* @return true: continue; false: abort
|
||||
*/
|
||||
public static boolean OnCoreError(CoreError error, String details) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
|
||||
String title, message;
|
||||
switch (error) {
|
||||
case ErrorSystemFiles: {
|
||||
title = emulationActivity.getString(R.string.system_archive_not_found);
|
||||
message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details);
|
||||
break;
|
||||
}
|
||||
case ErrorSavestate: {
|
||||
title = emulationActivity.getString(R.string.save_load_error);
|
||||
message = details;
|
||||
break;
|
||||
}
|
||||
case ErrorUnknown: {
|
||||
title = emulationActivity.getString(R.string.fatal_error);
|
||||
message = emulationActivity.getString(R.string.fatal_error_message);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message));
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized (coreErrorAlertLock) {
|
||||
try {
|
||||
coreErrorAlertLock.wait();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return coreErrorAlertResult;
|
||||
}
|
||||
|
||||
public static boolean isPortraitMode() {
|
||||
return CitraApplication.getAppContext().getResources().getConfiguration().orientation ==
|
||||
Configuration.ORIENTATION_PORTRAIT;
|
||||
}
|
||||
|
||||
public static int landscapeScreenLayout() {
|
||||
return EmulationMenuSettings.getLandscapeScreenLayout();
|
||||
}
|
||||
|
||||
public static boolean displayAlertMsg(final String caption, final String text,
|
||||
final boolean yesNo) {
|
||||
Log.error("[NativeLibrary] Alert: " + text);
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
boolean result = false;
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.");
|
||||
} else {
|
||||
// Create object used for waiting.
|
||||
final Object lock = new Object();
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(caption)
|
||||
.setMessage(text);
|
||||
|
||||
// If not yes/no dialog just have one button that dismisses modal,
|
||||
// otherwise have a yes and no button that sets alertResult accordingly.
|
||||
if (!yesNo) {
|
||||
builder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) ->
|
||||
{
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alertResult = false;
|
||||
|
||||
builder
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
|
||||
{
|
||||
alertResult = true;
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, (dialog, whichButton) ->
|
||||
{
|
||||
alertResult = false;
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread(builder::show);
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (yesNo)
|
||||
result = alertResult;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void retryDisplayAlertPrompt() {
|
||||
if (!alertPromptInProgress) {
|
||||
return;
|
||||
}
|
||||
displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show();
|
||||
}
|
||||
|
||||
public static String displayAlertPrompt(String caption, String text, int buttonConfig) {
|
||||
alertPromptCaption = caption;
|
||||
alertPromptButtonConfig = buttonConfig;
|
||||
alertPromptInProgress = true;
|
||||
|
||||
// Show the AlertDialog on the main thread
|
||||
sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show());
|
||||
|
||||
// Wait for the lock to notify that it is complete
|
||||
synchronized (alertPromptLock) {
|
||||
try {
|
||||
alertPromptLock.wait();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
alertPromptInProgress = false;
|
||||
|
||||
return alertPromptResult;
|
||||
}
|
||||
|
||||
public static MaterialAlertDialogBuilder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
alertPromptResult = "";
|
||||
alertPromptButton = 0;
|
||||
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
|
||||
// Set up the input
|
||||
alertPromptEditText = new EditText(CitraApplication.getAppContext());
|
||||
alertPromptEditText.setText(text);
|
||||
alertPromptEditText.setSingleLine();
|
||||
alertPromptEditText.setLayoutParams(params);
|
||||
|
||||
FrameLayout container = new FrameLayout(emulationActivity);
|
||||
container.addView(alertPromptEditText);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(caption)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
|
||||
{
|
||||
alertPromptButton = buttonConfig;
|
||||
alertPromptResult = alertPromptEditText.getText().toString();
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
})
|
||||
.setOnDismissListener(dialogInterface ->
|
||||
{
|
||||
alertPromptResult = "";
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
});
|
||||
|
||||
if (buttonConfig > 0) {
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
|
||||
{
|
||||
alertPromptResult = "";
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static int alertPromptButton() {
|
||||
return alertPromptButton;
|
||||
}
|
||||
|
||||
public static void exitEmulationActivity(int resultCode) {
|
||||
final int Success = 0;
|
||||
final int ErrorNotInitialized = 1;
|
||||
final int ErrorGetLoader = 2;
|
||||
final int ErrorSystemMode = 3;
|
||||
final int ErrorLoader = 4;
|
||||
final int ErrorLoader_ErrorEncrypted = 5;
|
||||
final int ErrorLoader_ErrorInvalidFormat = 6;
|
||||
final int ErrorSystemFiles = 7;
|
||||
final int ShutdownRequested = 11;
|
||||
final int ErrorUnknown = 12;
|
||||
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.");
|
||||
return;
|
||||
}
|
||||
|
||||
int captionId = R.string.loader_error_invalid_format;
|
||||
if (resultCode == ErrorLoader_ErrorEncrypted) {
|
||||
captionId = R.string.loader_error_encrypted;
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(captionId)
|
||||
.setMessage(Html.fromHtml("Please follow the guides to redump your <a href=\"https://citra-emu.org/wiki/dumping-game-cartridges/\">game cartidges</a> or <a href=\"https://citra-emu.org/wiki/dumping-installed-titles/\">installed titles</a>.", Html.FROM_HTML_MODE_LEGACY))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish())
|
||||
.setOnDismissListener(dialogInterface -> emulationActivity.finish());
|
||||
emulationActivity.runOnUiThread(() -> {
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
});
|
||||
}
|
||||
|
||||
public static void setEmulationActivity(EmulationActivity emulationActivity) {
|
||||
Log.verbose("[NativeLibrary] Registering EmulationActivity.");
|
||||
sEmulationActivity = new WeakReference<>(emulationActivity);
|
||||
}
|
||||
|
||||
public static void clearEmulationActivity() {
|
||||
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.");
|
||||
|
||||
sEmulationActivity.clear();
|
||||
}
|
||||
|
||||
private static final Object cameraPermissionLock = new Object();
|
||||
private static boolean cameraPermissionGranted = false;
|
||||
public static final int REQUEST_CODE_NATIVE_CAMERA = 800;
|
||||
|
||||
public static boolean RequestCameraPermission() {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission already granted
|
||||
return true;
|
||||
}
|
||||
emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA);
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized (cameraPermissionLock) {
|
||||
try {
|
||||
cameraPermissionLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return cameraPermissionGranted;
|
||||
}
|
||||
|
||||
public static void CameraPermissionResult(boolean granted) {
|
||||
cameraPermissionGranted = granted;
|
||||
synchronized (cameraPermissionLock) {
|
||||
cameraPermissionLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
private static final Object micPermissionLock = new Object();
|
||||
private static boolean micPermissionGranted = false;
|
||||
public static final int REQUEST_CODE_NATIVE_MIC = 900;
|
||||
|
||||
public static boolean RequestMicPermission() {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission already granted
|
||||
return true;
|
||||
}
|
||||
emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC);
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized (micPermissionLock) {
|
||||
try {
|
||||
micPermissionLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return micPermissionGranted;
|
||||
}
|
||||
|
||||
public static void MicPermissionResult(boolean granted) {
|
||||
micPermissionGranted = granted;
|
||||
synchronized (micPermissionLock) {
|
||||
micPermissionLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
||||
public static native void ReloadCameraDevices();
|
||||
|
||||
public static native boolean LoadAmiibo(String path);
|
||||
|
||||
public static native void RemoveAmiibo();
|
||||
|
||||
public static final int SAVESTATE_SLOT_COUNT = 10;
|
||||
|
||||
public static final class SavestateInfo {
|
||||
public int slot;
|
||||
public Date time;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static native SavestateInfo[] GetSavestateInfo();
|
||||
|
||||
public static native void SaveState(int slot);
|
||||
public static native void LoadState(int slot);
|
||||
|
||||
/**
|
||||
* Logs the Citra version, Android version and, CPU.
|
||||
*/
|
||||
public static native void LogDeviceInfo();
|
||||
|
||||
/**
|
||||
* Button type for use in onTouchEvent
|
||||
*/
|
||||
public static final class ButtonType {
|
||||
public static final int BUTTON_A = 700;
|
||||
public static final int BUTTON_B = 701;
|
||||
public static final int BUTTON_X = 702;
|
||||
public static final int BUTTON_Y = 703;
|
||||
public static final int BUTTON_START = 704;
|
||||
public static final int BUTTON_SELECT = 705;
|
||||
public static final int BUTTON_HOME = 706;
|
||||
public static final int BUTTON_ZL = 707;
|
||||
public static final int BUTTON_ZR = 708;
|
||||
public static final int DPAD_UP = 709;
|
||||
public static final int DPAD_DOWN = 710;
|
||||
public static final int DPAD_LEFT = 711;
|
||||
public static final int DPAD_RIGHT = 712;
|
||||
public static final int STICK_LEFT = 713;
|
||||
public static final int STICK_LEFT_UP = 714;
|
||||
public static final int STICK_LEFT_DOWN = 715;
|
||||
public static final int STICK_LEFT_LEFT = 716;
|
||||
public static final int STICK_LEFT_RIGHT = 717;
|
||||
public static final int STICK_C = 718;
|
||||
public static final int STICK_C_UP = 719;
|
||||
public static final int STICK_C_DOWN = 720;
|
||||
public static final int STICK_C_LEFT = 771;
|
||||
public static final int STICK_C_RIGHT = 772;
|
||||
public static final int TRIGGER_L = 773;
|
||||
public static final int TRIGGER_R = 774;
|
||||
public static final int DPAD = 780;
|
||||
public static final int BUTTON_DEBUG = 781;
|
||||
public static final int BUTTON_GPIO14 = 782;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button states
|
||||
*/
|
||||
public static final class ButtonState {
|
||||
public static final int RELEASED = 0;
|
||||
public static final int PRESSED = 1;
|
||||
}
|
||||
public static boolean createFile(String directory, String filename) {
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
return CitraApplication.documentsTree.createFile(directory, filename);
|
||||
}
|
||||
return FileUtil.createFile(CitraApplication.getAppContext(), directory, filename) != null;
|
||||
}
|
||||
|
||||
public static boolean createDir(String directory, String directoryName) {
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
return CitraApplication.documentsTree.createDir(directory, directoryName);
|
||||
}
|
||||
return FileUtil.createDir(CitraApplication.getAppContext(), directory, directoryName) != null;
|
||||
}
|
||||
|
||||
public static int openContentUri(String path, String openMode) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.openContentUri(path, openMode);
|
||||
}
|
||||
return FileUtil.openContentUri(CitraApplication.getAppContext(), path, openMode);
|
||||
}
|
||||
|
||||
public static String[] getFilesName(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.getFilesName(path);
|
||||
}
|
||||
return FileUtil.getFilesName(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static long getSize(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.getFileSize(path);
|
||||
}
|
||||
return FileUtil.getFileSize(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean fileExists(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.Exists(path);
|
||||
}
|
||||
return FileUtil.Exists(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean isDirectory(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.isDirectory(path);
|
||||
}
|
||||
return FileUtil.isDirectory(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean copyFile(String sourcePath, String destinationParentPath, String destinationFilename) {
|
||||
if (FileUtil.isNativePath(sourcePath) && FileUtil.isNativePath(destinationParentPath)) {
|
||||
return CitraApplication.documentsTree.copyFile(sourcePath, destinationParentPath, destinationFilename);
|
||||
}
|
||||
return FileUtil.copyFile(CitraApplication.getAppContext(), sourcePath, destinationParentPath, destinationFilename);
|
||||
}
|
||||
|
||||
public static boolean renameFile(String path, String destinationFilename) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.renameFile(path, destinationFilename);
|
||||
}
|
||||
return FileUtil.renameFile(CitraApplication.getAppContext(), path, destinationFilename);
|
||||
}
|
||||
|
||||
public static boolean deleteDocument(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.deleteDocument(path);
|
||||
}
|
||||
return FileUtil.deleteDocument(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
}
|
@ -1,785 +0,0 @@
|
||||
package org.citra.citra_emu.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
||||
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.camera.StillImageCameraHelper;
|
||||
import org.citra.citra_emu.fragments.EmulationFragment;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
import org.citra.citra_emu.utils.ControllerMappingHelper;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.ForegroundService;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static android.Manifest.permission.CAMERA;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.slider.Slider;
|
||||
|
||||
public final class EmulationActivity extends AppCompatActivity {
|
||||
public static final String EXTRA_SELECTED_GAME = "SelectedGame";
|
||||
public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
|
||||
public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0;
|
||||
public static final int MENU_ACTION_TOGGLE_CONTROLS = 1;
|
||||
public static final int MENU_ACTION_ADJUST_SCALE = 2;
|
||||
public static final int MENU_ACTION_EXIT = 3;
|
||||
public static final int MENU_ACTION_SHOW_FPS = 4;
|
||||
public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5;
|
||||
public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6;
|
||||
public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7;
|
||||
public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8;
|
||||
public static final int MENU_ACTION_SWAP_SCREENS = 9;
|
||||
public static final int MENU_ACTION_RESET_OVERLAY = 10;
|
||||
public static final int MENU_ACTION_SHOW_OVERLAY = 11;
|
||||
public static final int MENU_ACTION_OPEN_SETTINGS = 12;
|
||||
public static final int MENU_ACTION_LOAD_AMIIBO = 13;
|
||||
public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
|
||||
public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
|
||||
public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
|
||||
public static final int MENU_ACTION_OPEN_CHEATS = 17;
|
||||
public static final int MENU_ACTION_CLOSE_GAME = 18;
|
||||
|
||||
public static final int REQUEST_SELECT_AMIIBO = 2;
|
||||
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
||||
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
|
||||
|
||||
private final ActivityResultLauncher<Boolean> mOpenFileLauncher =
|
||||
registerForActivityResult(new OpenFileResultContract(), result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
|
||||
result, getApplicationContext(), Collections.singletonList("bin"));
|
||||
if (selectedFiles == null)
|
||||
return;
|
||||
|
||||
onAmiiboSelected(selectedFiles[0]);
|
||||
});
|
||||
|
||||
static {
|
||||
buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
|
||||
EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_toggle_controls,
|
||||
EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_show_fps,
|
||||
EmulationActivity.MENU_ACTION_SHOW_FPS);
|
||||
buttonsActionsMap.append(R.id.menu_screen_layout_landscape,
|
||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE);
|
||||
buttonsActionsMap.append(R.id.menu_screen_layout_portrait,
|
||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT);
|
||||
buttonsActionsMap.append(R.id.menu_screen_layout_single,
|
||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE);
|
||||
buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside,
|
||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_swap_screens,
|
||||
EmulationActivity.MENU_ACTION_SWAP_SCREENS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center,
|
||||
EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable,
|
||||
EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME);
|
||||
}
|
||||
|
||||
private EmulationFragment mEmulationFragment;
|
||||
private SharedPreferences mPreferences;
|
||||
private ControllerMappingHelper mControllerMappingHelper;
|
||||
private Intent foregroundService;
|
||||
private boolean activityRecreated;
|
||||
private String mSelectedTitle;
|
||||
private String mPath;
|
||||
|
||||
public static void launch(FragmentActivity activity, String path, String title) {
|
||||
Intent launcher = new Intent(activity, EmulationActivity.class);
|
||||
|
||||
launcher.putExtra(EXTRA_SELECTED_GAME, path);
|
||||
launcher.putExtra(EXTRA_SELECTED_TITLE, title);
|
||||
activity.startActivity(launcher);
|
||||
}
|
||||
|
||||
public static void tryDismissRunningNotification(Activity activity) {
|
||||
NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
stopService(foregroundService);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
ThemeUtil.applyTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// Get params we were passed
|
||||
Intent gameToEmulate = getIntent();
|
||||
mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME);
|
||||
mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE);
|
||||
activityRecreated = false;
|
||||
} else {
|
||||
activityRecreated = true;
|
||||
restoreState(savedInstanceState);
|
||||
}
|
||||
|
||||
mControllerMappingHelper = new ControllerMappingHelper();
|
||||
|
||||
// Set these options now so that the SurfaceView the game renders into is the right size.
|
||||
enableFullscreenImmersive();
|
||||
|
||||
setContentView(R.layout.activity_emulation);
|
||||
|
||||
// Find or create the EmulationFragment
|
||||
mEmulationFragment = (EmulationFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.frame_emulation_fragment);
|
||||
if (mEmulationFragment == null) {
|
||||
mEmulationFragment = EmulationFragment.newInstance(mPath);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.frame_emulation_fragment, mEmulationFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
setTitle(mSelectedTitle);
|
||||
|
||||
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
// Start a foreground service to prevent the app from getting killed in the background
|
||||
foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
|
||||
startForegroundService(foregroundService);
|
||||
|
||||
// Override Citra core INI with the one set by our in game menu
|
||||
NativeLibrary.SwapScreens(EmulationMenuSettings.getSwapScreens(),
|
||||
getWindowManager().getDefaultDisplay().getRotation());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putString(EXTRA_SELECTED_GAME, mPath);
|
||||
outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
protected void restoreState(Bundle savedInstanceState) {
|
||||
mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
|
||||
mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
|
||||
|
||||
// If an alert prompt was in progress when state was restored, retry displaying it
|
||||
NativeLibrary.retryDisplayAlertPrompt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestart() {
|
||||
super.onRestart();
|
||||
NativeLibrary.ReloadCameraDevices();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
View anchor = findViewById(R.id.menu_anchor);
|
||||
PopupMenu popupMenu = new PopupMenu(this, anchor);
|
||||
onCreateOptionsMenu(popupMenu.getMenu(), popupMenu.getMenuInflater());
|
||||
updateSavestateMenuOptions(popupMenu.getMenu());
|
||||
popupMenu.setOnMenuItemClickListener(this::onOptionsItemSelected);
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA:
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
shouldShowRequestPermissionRationale(CAMERA)) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.camera)
|
||||
.setMessage(R.string.camera_permission_needed)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
break;
|
||||
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
shouldShowRequestPermissionRationale(RECORD_AUDIO)) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.microphone)
|
||||
.setMessage(R.string.microphone_permission_needed)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
break;
|
||||
default:
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void onEmulationStarted() {
|
||||
Toast.makeText(this, getString(R.string.emulation_menu_help), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void enableFullscreenImmersive() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
onCreateOptionsMenu(menu, getMenuInflater());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_emulation, menu);
|
||||
|
||||
int layoutOptionMenuItem = R.id.menu_screen_layout_landscape;
|
||||
switch (EmulationMenuSettings.getLandscapeScreenLayout()) {
|
||||
case EmulationMenuSettings.LayoutOption_SingleScreen:
|
||||
layoutOptionMenuItem = R.id.menu_screen_layout_single;
|
||||
break;
|
||||
case EmulationMenuSettings.LayoutOption_SideScreen:
|
||||
layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside;
|
||||
break;
|
||||
case EmulationMenuSettings.LayoutOption_MobilePortrait:
|
||||
layoutOptionMenuItem = R.id.menu_screen_layout_portrait;
|
||||
break;
|
||||
}
|
||||
|
||||
menu.findItem(layoutOptionMenuItem).setChecked(true);
|
||||
menu.findItem(R.id.menu_emulation_joystick_rel_center).setChecked(EmulationMenuSettings.getJoystickRelCenter());
|
||||
menu.findItem(R.id.menu_emulation_dpad_slide_enable).setChecked(EmulationMenuSettings.getDpadSlideEnable());
|
||||
menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps());
|
||||
menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens());
|
||||
menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay());
|
||||
}
|
||||
|
||||
private void DisplaySavestateWarning() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
if (preferences.getBoolean("savestateWarningShown", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.dialog_checkbox, null);
|
||||
CheckBox checkBox = view.findViewById(R.id.checkBox);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.savestate_warning_title)
|
||||
.setMessage(R.string.savestate_warning_message)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
updateSavestateMenuOptions(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateSavestateMenuOptions(Menu menu) {
|
||||
final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo();
|
||||
if (savestates == null) {
|
||||
menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
|
||||
menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
|
||||
return;
|
||||
}
|
||||
menu.findItem(R.id.menu_emulation_save_state).setVisible(true);
|
||||
menu.findItem(R.id.menu_emulation_load_state).setVisible(true);
|
||||
|
||||
final SubMenu saveStateMenu = menu.findItem(R.id.menu_emulation_save_state).getSubMenu();
|
||||
final SubMenu loadStateMenu = menu.findItem(R.id.menu_emulation_load_state).getSubMenu();
|
||||
saveStateMenu.clear();
|
||||
loadStateMenu.clear();
|
||||
|
||||
// Update savestates information
|
||||
for (int i = 0; i < NativeLibrary.SAVESTATE_SLOT_COUNT; ++i) {
|
||||
final int slot = i + 1;
|
||||
final String text = getString(R.string.emulation_empty_state_slot, slot);
|
||||
saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> {
|
||||
DisplaySavestateWarning();
|
||||
NativeLibrary.SaveState(slot);
|
||||
return true;
|
||||
});
|
||||
loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> {
|
||||
NativeLibrary.LoadState(slot);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
for (final NativeLibrary.SavestateInfo info : savestates) {
|
||||
final String text = getString(R.string.emulation_occupied_state_slot, info.slot, info.time);
|
||||
saveStateMenu.getItem(info.slot - 1).setTitle(text);
|
||||
loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WrongConstant")
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int action = buttonsActionsMap.get(item.getItemId(), -1);
|
||||
|
||||
switch (action) {
|
||||
// Edit the placement of the controls
|
||||
case MENU_ACTION_EDIT_CONTROLS_PLACEMENT:
|
||||
editControlsPlacement();
|
||||
break;
|
||||
|
||||
// Enable/Disable specific buttons or the entire input overlay.
|
||||
case MENU_ACTION_TOGGLE_CONTROLS:
|
||||
toggleControls();
|
||||
break;
|
||||
|
||||
// Adjust the scale of the overlay controls.
|
||||
case MENU_ACTION_ADJUST_SCALE:
|
||||
adjustScale();
|
||||
break;
|
||||
|
||||
// Toggle the visibility of the Performance stats TextView
|
||||
case MENU_ACTION_SHOW_FPS: {
|
||||
final boolean isEnabled = !EmulationMenuSettings.getShowFps();
|
||||
EmulationMenuSettings.setShowFps(isEnabled);
|
||||
item.setChecked(isEnabled);
|
||||
|
||||
mEmulationFragment.updateShowFpsOverlay();
|
||||
break;
|
||||
}
|
||||
// Sets the screen layout to Landscape
|
||||
case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE:
|
||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item);
|
||||
break;
|
||||
|
||||
// Sets the screen layout to Portrait
|
||||
case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT:
|
||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item);
|
||||
break;
|
||||
|
||||
// Sets the screen layout to Single
|
||||
case MENU_ACTION_SCREEN_LAYOUT_SINGLE:
|
||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item);
|
||||
break;
|
||||
|
||||
// Sets the screen layout to Side by Side
|
||||
case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE:
|
||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item);
|
||||
break;
|
||||
|
||||
// Swap the top and bottom screen locations
|
||||
case MENU_ACTION_SWAP_SCREENS: {
|
||||
final boolean isEnabled = !EmulationMenuSettings.getSwapScreens();
|
||||
EmulationMenuSettings.setSwapScreens(isEnabled);
|
||||
item.setChecked(isEnabled);
|
||||
|
||||
NativeLibrary.SwapScreens(isEnabled, getWindowManager().getDefaultDisplay()
|
||||
.getRotation());
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset overlay placement
|
||||
case MENU_ACTION_RESET_OVERLAY:
|
||||
resetOverlay();
|
||||
break;
|
||||
|
||||
// Show or hide overlay
|
||||
case MENU_ACTION_SHOW_OVERLAY: {
|
||||
final boolean isEnabled = !EmulationMenuSettings.getShowOverlay();
|
||||
EmulationMenuSettings.setShowOverlay(isEnabled);
|
||||
item.setChecked(isEnabled);
|
||||
|
||||
mEmulationFragment.refreshInputOverlay();
|
||||
break;
|
||||
}
|
||||
|
||||
case MENU_ACTION_EXIT:
|
||||
mEmulationFragment.stopEmulation();
|
||||
finish();
|
||||
break;
|
||||
|
||||
case MENU_ACTION_OPEN_SETTINGS:
|
||||
SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
|
||||
break;
|
||||
|
||||
case MENU_ACTION_LOAD_AMIIBO:
|
||||
mOpenFileLauncher.launch(false);
|
||||
break;
|
||||
|
||||
case MENU_ACTION_REMOVE_AMIIBO:
|
||||
RemoveAmiibo();
|
||||
break;
|
||||
|
||||
case MENU_ACTION_JOYSTICK_REL_CENTER:
|
||||
final boolean isJoystickRelCenterEnabled = !EmulationMenuSettings.getJoystickRelCenter();
|
||||
EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled);
|
||||
item.setChecked(isJoystickRelCenterEnabled);
|
||||
break;
|
||||
|
||||
case MENU_ACTION_DPAD_SLIDE_ENABLE:
|
||||
final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable();
|
||||
EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled);
|
||||
item.setChecked(isDpadSlideEnabled);
|
||||
break;
|
||||
|
||||
case MENU_ACTION_OPEN_CHEATS:
|
||||
CheatsActivity.launch(this, NativeLibrary.GetRunningTitleId());
|
||||
break;
|
||||
|
||||
case MENU_ACTION_CLOSE_GAME:
|
||||
NativeLibrary.PauseEmulation();
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.emulation_close_game)
|
||||
.setMessage(R.string.emulation_close_game_message)
|
||||
.setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
|
||||
{
|
||||
mEmulationFragment.stopEmulation();
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.UnPauseEmulation())
|
||||
.setOnCancelListener(dialogInterface -> NativeLibrary.UnPauseEmulation())
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeScreenOrientation(int layoutOption, MenuItem item) {
|
||||
item.setChecked(true);
|
||||
NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
|
||||
.getRotation());
|
||||
EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
|
||||
}
|
||||
|
||||
private void editControlsPlacement() {
|
||||
if (mEmulationFragment.isConfiguringControls()) {
|
||||
mEmulationFragment.stopConfiguringControls();
|
||||
} else {
|
||||
mEmulationFragment.startConfiguringControls();
|
||||
}
|
||||
}
|
||||
|
||||
// Gets button presses
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int action;
|
||||
int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
|
||||
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
// Handling the case where the back button is pressed.
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normal key events.
|
||||
action = NativeLibrary.ButtonState.PRESSED;
|
||||
break;
|
||||
case KeyEvent.ACTION_UP:
|
||||
action = NativeLibrary.ButtonState.RELEASED;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
InputDevice input = event.getDevice();
|
||||
|
||||
if (input == null) {
|
||||
// Controller was disconnected
|
||||
return false;
|
||||
}
|
||||
|
||||
return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
|
||||
super.onActivityResult(requestCode, resultCode, result);
|
||||
if (requestCode == StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER) {
|
||||
StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAmiiboSelected(String selectedFile) {
|
||||
boolean success = NativeLibrary.LoadAmiibo(selectedFile);
|
||||
|
||||
if (!success) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.amiibo_load_error)
|
||||
.setMessage(R.string.amiibo_load_error_message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAmiibo() {
|
||||
NativeLibrary.RemoveAmiibo();
|
||||
}
|
||||
|
||||
private void toggleControls() {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
boolean[] enabledButtons = new boolean[14];
|
||||
|
||||
for (int i = 0; i < enabledButtons.length; i++) {
|
||||
// Buttons that are disabled by default
|
||||
boolean defaultValue = true;
|
||||
switch (i) {
|
||||
case 6: // ZL
|
||||
case 7: // ZR
|
||||
case 12: // C-stick
|
||||
defaultValue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue);
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.emulation_toggle_controls)
|
||||
.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons,
|
||||
(dialog, indexSelected, isChecked) -> editor
|
||||
.putBoolean("buttonToggle" + indexSelected, isChecked))
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
|
||||
{
|
||||
editor.apply();
|
||||
mEmulationFragment.refreshInputOverlay();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void adjustScale() {
|
||||
LayoutInflater inflater = LayoutInflater.from(this);
|
||||
View view = inflater.inflate(R.layout.dialog_slider, null);
|
||||
|
||||
final Slider slider = view.findViewById(R.id.slider);
|
||||
final TextView textValue = view.findViewById(R.id.text_value);
|
||||
final TextView units = view.findViewById(R.id.text_units);
|
||||
|
||||
slider.setValueTo(150);
|
||||
slider.setValue(mPreferences.getInt("controlScale", 50));
|
||||
slider.addOnChangeListener((slider1, progress, fromUser) -> {
|
||||
textValue.setText(String.valueOf((int) progress + 50));
|
||||
setControlScale((int) slider1.getValue());
|
||||
});
|
||||
|
||||
textValue.setText(String.valueOf((int) slider.getValue() + 50));
|
||||
units.setText("%");
|
||||
|
||||
final int previousProgress = (int) slider.getValue();
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.emulation_control_scale)
|
||||
.setView(view)
|
||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> setControlScale(previousProgress))
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> setControlScale((int) slider.getValue()))
|
||||
.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> setControlScale(50))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void setControlScale(int scale) {
|
||||
SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putInt("controlScale", scale);
|
||||
editor.apply();
|
||||
mEmulationFragment.refreshInputOverlay();
|
||||
}
|
||||
|
||||
private void resetOverlay() {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.emulation_touch_overlay_reset))
|
||||
.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent event) {
|
||||
if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) {
|
||||
return super.dispatchGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
// Don't attempt to do anything if we are disconnecting a device.
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
InputDevice input = event.getDevice();
|
||||
List<InputDevice.MotionRange> motions = input.getMotionRanges();
|
||||
|
||||
float[] axisValuesCirclePad = {0.0f, 0.0f};
|
||||
float[] axisValuesCStick = {0.0f, 0.0f};
|
||||
float[] axisValuesDPad = {0.0f, 0.0f};
|
||||
boolean isTriggerPressedLMapped = false;
|
||||
boolean isTriggerPressedRMapped = false;
|
||||
boolean isTriggerPressedZLMapped = false;
|
||||
boolean isTriggerPressedZRMapped = false;
|
||||
boolean isTriggerPressedL = false;
|
||||
boolean isTriggerPressedR = false;
|
||||
boolean isTriggerPressedZL = false;
|
||||
boolean isTriggerPressedZR = false;
|
||||
|
||||
for (InputDevice.MotionRange range : motions) {
|
||||
int axis = range.getAxis();
|
||||
float origValue = event.getAxisValue(axis);
|
||||
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
|
||||
int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
|
||||
int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
|
||||
|
||||
if (nextMapping == -1 || guestOrientation == -1) {
|
||||
// Axis is unmapped
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) {
|
||||
// Skip joystick wobble
|
||||
value = 0.f;
|
||||
}
|
||||
|
||||
if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) {
|
||||
axisValuesCirclePad[guestOrientation] = value;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.STICK_C) {
|
||||
axisValuesCStick[guestOrientation] = value;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.DPAD) {
|
||||
axisValuesDPad[guestOrientation] = value;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) {
|
||||
isTriggerPressedLMapped = true;
|
||||
isTriggerPressedL = value != 0.f;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) {
|
||||
isTriggerPressedRMapped = true;
|
||||
isTriggerPressedR = value != 0.f;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) {
|
||||
isTriggerPressedZLMapped = true;
|
||||
isTriggerPressedZL = value != 0.f;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) {
|
||||
isTriggerPressedZRMapped = true;
|
||||
isTriggerPressedZR = value != 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// Circle-Pad and C-Stick status
|
||||
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
|
||||
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
|
||||
|
||||
// Triggers L/R and ZL/ZR
|
||||
if (isTriggerPressedLMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedRMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedZLMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedZRMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
|
||||
// Work-around to allow D-pad axis to be bound to emulated buttons
|
||||
if (axisValuesDPad[0] == 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[0] < 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[0] > 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
|
||||
}
|
||||
if (axisValuesDPad[1] == 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[1] < 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[1] > 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isActivityRecreated() {
|
||||
return activityRecreated;
|
||||
}
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE,
|
||||
MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE,
|
||||
MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE,
|
||||
MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS})
|
||||
public @interface MenuAction {
|
||||
}
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
package org.citra.citra_emu.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.PicassoUtils;
|
||||
import org.citra.citra_emu.viewholders.GameViewHolder;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* This adapter gets its information from a database Cursor. This fact, paired with the usage of
|
||||
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
|
||||
* large dataset.
|
||||
*/
|
||||
public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> {
|
||||
private Cursor mCursor;
|
||||
private GameDataSetObserver mObserver;
|
||||
|
||||
private boolean mDatasetValid;
|
||||
private long mLastClickTime = 0;
|
||||
|
||||
/**
|
||||
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
|
||||
* display no data until a Cursor is supplied by a CursorLoader.
|
||||
*/
|
||||
public GameAdapter() {
|
||||
mDatasetValid = false;
|
||||
mObserver = new GameDataSetObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager when it is necessary to create a new view.
|
||||
*
|
||||
* @param parent The RecyclerView (I think?) the created view will be thrown into.
|
||||
* @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
|
||||
* @return The created ViewHolder with references to all the child view's members.
|
||||
*/
|
||||
@Override
|
||||
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
// Create a new view.
|
||||
View gameCard = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.card_game, parent, false);
|
||||
|
||||
gameCard.setOnClickListener(this::onClick);
|
||||
gameCard.setOnLongClickListener(this::onLongClick);
|
||||
|
||||
// Use that view to create a ViewHolder.
|
||||
return new GameViewHolder(gameCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager when a new view is not necessary because we can recycle
|
||||
* an existing one (for example, if a view just scrolled onto the screen from the bottom, we
|
||||
* can use the view that just scrolled off the top instead of inflating a new one.)
|
||||
*
|
||||
* @param holder A ViewHolder representing the view we're recycling.
|
||||
* @param position The position of the 'new' view in the dataset.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
|
||||
if (mDatasetValid) {
|
||||
if (mCursor.moveToPosition(position)) {
|
||||
PicassoUtils.loadGameIcon(holder.imageIcon,
|
||||
mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
|
||||
|
||||
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
|
||||
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
||||
|
||||
String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
||||
String filename;
|
||||
if (FileUtil.isNativePath(filepath)) {
|
||||
filename = CitraApplication.documentsTree.getFilename(filepath);
|
||||
} else {
|
||||
filename = FileUtil.getFilename(CitraApplication.getAppContext(), filepath);
|
||||
}
|
||||
holder.textFileName.setText(filename);
|
||||
|
||||
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
|
||||
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
|
||||
holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
||||
holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
|
||||
holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
|
||||
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
|
||||
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY);
|
||||
|
||||
final int backgroundColorId = isValidGame(holder.path) ? R.attr.colorSurface : R.attr.colorErrorContainer;
|
||||
View itemView = holder.getItemView();
|
||||
itemView.setBackgroundColor(MaterialColors.getColor(itemView, backgroundColorId));
|
||||
} else {
|
||||
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
|
||||
}
|
||||
} else {
|
||||
Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager to find out how much data we have.
|
||||
*
|
||||
* @return Size of the dataset.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (mDatasetValid && mCursor != null) {
|
||||
return mCursor.getCount();
|
||||
}
|
||||
Log.error("[GameAdapter] Dataset is not valid.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of the _id column for a given row.
|
||||
*
|
||||
* @param position The row for which Android wants an ID.
|
||||
* @return A valid ID from the database, or 0 if not available.
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (mDatasetValid && mCursor != null) {
|
||||
if (mCursor.moveToPosition(position)) {
|
||||
return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
|
||||
}
|
||||
}
|
||||
|
||||
Log.error("[GameAdapter] Dataset is not valid.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell Android whether or not each item in the dataset has a stable identifier.
|
||||
* Which it does, because it's a database, so always tell Android 'true'.
|
||||
*
|
||||
* @param hasStableIds ignored.
|
||||
*/
|
||||
@Override
|
||||
public void setHasStableIds(boolean hasStableIds) {
|
||||
super.setHasStableIds(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a load is finished, call this to replace the existing data with the newly-loaded
|
||||
* data.
|
||||
*
|
||||
* @param cursor The newly-loaded Cursor.
|
||||
*/
|
||||
public void swapCursor(Cursor cursor) {
|
||||
// Sanity check.
|
||||
if (cursor == mCursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before getting rid of the old cursor, disassociate it from the Observer.
|
||||
final Cursor oldCursor = mCursor;
|
||||
if (oldCursor != null && mObserver != null) {
|
||||
oldCursor.unregisterDataSetObserver(mObserver);
|
||||
}
|
||||
|
||||
mCursor = cursor;
|
||||
if (mCursor != null) {
|
||||
// Attempt to associate the new Cursor with the Observer.
|
||||
if (mObserver != null) {
|
||||
mCursor.registerDataSetObserver(mObserver);
|
||||
}
|
||||
|
||||
mDatasetValid = true;
|
||||
} else {
|
||||
mDatasetValid = false;
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the game that was clicked on.
|
||||
*
|
||||
* @param view The view representing the game the user wants to play.
|
||||
*/
|
||||
private void onClick(View view) {
|
||||
// Double-click prevention, using threshold of 1000 ms
|
||||
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
|
||||
return;
|
||||
}
|
||||
mLastClickTime = SystemClock.elapsedRealtime();
|
||||
|
||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
||||
|
||||
EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cheats settings for the game that was clicked on.
|
||||
*
|
||||
* @param view The view representing the game the user wants to play.
|
||||
*/
|
||||
private boolean onLongClick(View view) {
|
||||
Context context = view.getContext();
|
||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
||||
|
||||
final long titleId = NativeLibrary.GetTitleId(holder.path);
|
||||
|
||||
if (titleId == 0) {
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.properties)
|
||||
.setMessage(R.string.properties_not_loaded)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
} else {
|
||||
CheatsActivity.launch(context, titleId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isValidGame(String path) {
|
||||
return Stream.of(
|
||||
".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
|
||||
}
|
||||
|
||||
private final class GameDataSetObserver extends DataSetObserver {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
super.onChanged();
|
||||
|
||||
mDatasetValid = true;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidated() {
|
||||
super.onInvalidated();
|
||||
|
||||
mDatasetValid = false;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.camera;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.utils.PicassoUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
// Used in native code.
|
||||
public final class StillImageCameraHelper {
|
||||
public static final int REQUEST_CAMERA_FILE_PICKER = 1;
|
||||
private static final Object filePickerLock = new Object();
|
||||
private static @Nullable
|
||||
String filePickerPath;
|
||||
|
||||
// Opens file picker for camera.
|
||||
public static @Nullable
|
||||
String OpenFilePicker() {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
|
||||
// At this point, we are assuming that we already have permissions as they are
|
||||
// needed to launch a game
|
||||
emulationActivity.runOnUiThread(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK);
|
||||
intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
emulationActivity.startActivityForResult(
|
||||
Intent.createChooser(intent,
|
||||
emulationActivity.getString(R.string.camera_select_image)),
|
||||
REQUEST_CAMERA_FILE_PICKER);
|
||||
});
|
||||
|
||||
synchronized (filePickerLock) {
|
||||
try {
|
||||
filePickerLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return filePickerPath;
|
||||
}
|
||||
|
||||
// Called from EmulationActivity.
|
||||
public static void OnFilePickerResult(Intent result) {
|
||||
filePickerPath = result == null ? null : result.getDataString();
|
||||
|
||||
synchronized (filePickerLock) {
|
||||
filePickerLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Blocking call. Load image from file and crop/resize it to fit in width x height.
|
||||
@Nullable
|
||||
public static Bitmap LoadImageFromFile(String uri, int width, int height) {
|
||||
return PicassoUtils.LoadBitmapFromFile(uri, width, height);
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import java.util.Objects;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
public class CitraDirectoryDialog extends DialogFragment {
|
||||
public static final String TAG = "citra_directory_dialog_fragment";
|
||||
|
||||
private static final String MOVE_DATE_ENABLE = "IS_MODE_DATA_ENABLE";
|
||||
|
||||
TextView pathView;
|
||||
|
||||
TextView spaceView;
|
||||
|
||||
CheckBox checkBox;
|
||||
|
||||
AlertDialog dialog;
|
||||
|
||||
Listener listener;
|
||||
|
||||
public interface Listener {
|
||||
void onPressPositiveButton(boolean moveData, Uri path);
|
||||
}
|
||||
|
||||
public static CitraDirectoryDialog newInstance(String path, Listener listener) {
|
||||
CitraDirectoryDialog frag = new CitraDirectoryDialog();
|
||||
frag.listener = listener;
|
||||
Bundle args = new Bundle();
|
||||
args.putString("path", path);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = requireActivity();
|
||||
final Uri path = Uri.parse(Objects.requireNonNull(requireArguments().getString("path")));
|
||||
SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
String freeSpaceText =
|
||||
getResources().getString(R.string.free_space, FileUtil.getFreeSpace(activity, path));
|
||||
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.dialog_citra_directory, null);
|
||||
|
||||
checkBox = view.findViewById(R.id.checkBox);
|
||||
pathView = view.findViewById(R.id.path);
|
||||
spaceView = view.findViewById(R.id.space);
|
||||
|
||||
checkBox.setChecked(mPreferences.getBoolean(MOVE_DATE_ENABLE, true));
|
||||
if (!PermissionsHandler.hasWriteAccess(activity)) {
|
||||
checkBox.setVisibility(View.GONE);
|
||||
}
|
||||
checkBox.setOnCheckedChangeListener(
|
||||
(v, isChecked)
|
||||
// record move data selection with SharedPreferences
|
||||
-> mPreferences.edit().putBoolean(MOVE_DATE_ENABLE, checkBox.isChecked()).apply());
|
||||
|
||||
pathView.setText(path.getPath());
|
||||
spaceView.setText(freeSpaceText);
|
||||
|
||||
setCancelable(false);
|
||||
|
||||
dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setView(view)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.app_name)
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(d, v) -> listener.onPressPositiveButton(checkBox.isChecked(), path))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
return dialog;
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import org.citra.citra_emu.R;
|
||||
|
||||
public class CopyDirProgressDialog extends DialogFragment {
|
||||
public static final String TAG = "copy_dir_progress_dialog";
|
||||
ProgressBar progressBar;
|
||||
|
||||
TextView progressText;
|
||||
|
||||
AlertDialog dialog;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = requireActivity();
|
||||
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
|
||||
|
||||
progressBar = view.findViewById(R.id.progress_bar);
|
||||
progressText = view.findViewById(R.id.progress_text);
|
||||
progressText.setText("");
|
||||
|
||||
setCancelable(false);
|
||||
|
||||
dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setView(view)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.move_data)
|
||||
.setMessage("")
|
||||
.create();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public void onUpdateSearchProgress(String msg) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
dialog.setMessage(getResources().getString(R.string.searching_direcotry, msg));
|
||||
});
|
||||
}
|
||||
|
||||
public void onUpdateCopyProgress(String msg, int progress, int max) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
progressBar.setProgress(progress);
|
||||
progressBar.setMax(max);
|
||||
progressText.setText(String.format("%d/%d", progress, max));
|
||||
dialog.setMessage(getResources().getString(R.string.copy_file_name, msg));
|
||||
});
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AlertDialog} derivative that listens for
|
||||
* motion events from controllers and joysticks.
|
||||
*/
|
||||
public final class MotionAlertDialog extends AlertDialog {
|
||||
// The selected input preference
|
||||
private final InputBindingSetting setting;
|
||||
private final ArrayList<Float> mPreviousValues = new ArrayList<>();
|
||||
private int mPrevDeviceId = 0;
|
||||
private boolean mWaitingForEvent = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context The current {@link Context}.
|
||||
* @param setting The Preference to show this dialog for.
|
||||
*/
|
||||
public MotionAlertDialog(Context context, InputBindingSetting setting) {
|
||||
super(context);
|
||||
|
||||
this.setting = setting;
|
||||
}
|
||||
|
||||
public boolean onKeyEvent(int keyCode, KeyEvent event) {
|
||||
Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_UP:
|
||||
setting.onKeyInput(event);
|
||||
dismiss();
|
||||
// Even if we ignore the key, we still consume it. Thus return true regardless.
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
|
||||
return super.onKeyLongPress(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// Handle this key if we care about it, otherwise pass it down the framework
|
||||
return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
|
||||
// Handle this event if we care about it, otherwise pass it down the framework
|
||||
return onMotionEvent(event) || super.dispatchGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
private boolean onMotionEvent(MotionEvent event) {
|
||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
|
||||
return false;
|
||||
if (event.getAction() != MotionEvent.ACTION_MOVE)
|
||||
return false;
|
||||
|
||||
InputDevice input = event.getDevice();
|
||||
|
||||
List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
|
||||
|
||||
if (input.getId() != mPrevDeviceId) {
|
||||
mPreviousValues.clear();
|
||||
}
|
||||
mPrevDeviceId = input.getId();
|
||||
boolean firstEvent = mPreviousValues.isEmpty();
|
||||
|
||||
int numMovedAxis = 0;
|
||||
float axisMoveValue = 0.0f;
|
||||
InputDevice.MotionRange lastMovedRange = null;
|
||||
char lastMovedDir = '?';
|
||||
if (mWaitingForEvent) {
|
||||
for (int i = 0; i < motionRanges.size(); i++) {
|
||||
InputDevice.MotionRange range = motionRanges.get(i);
|
||||
int axis = range.getAxis();
|
||||
float origValue = event.getAxisValue(axis);
|
||||
float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue);
|
||||
if (firstEvent) {
|
||||
mPreviousValues.add(value);
|
||||
} else {
|
||||
float previousValue = mPreviousValues.get(i);
|
||||
|
||||
// Only handle the axes that are not neutral (more than 0.5)
|
||||
// but ignore any axis that has a constant value (e.g. always 1)
|
||||
if (Math.abs(value) > 0.5f && value != previousValue) {
|
||||
// It is common to have multiple axes with the same physical input. For example,
|
||||
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
|
||||
// To handle this, we ignore an axis motion that's the exact same as a motion
|
||||
// we already saw. This way, we ignore axes with two names, but catch the case
|
||||
// where a joystick is moved in two directions.
|
||||
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
|
||||
if (value != axisMoveValue) {
|
||||
axisMoveValue = value;
|
||||
numMovedAxis++;
|
||||
lastMovedRange = range;
|
||||
lastMovedDir = value < 0.0f ? '-' : '+';
|
||||
}
|
||||
}
|
||||
// Special case for d-pads (axis value jumps between 0 and 1 without any values
|
||||
// in between). Without this, the user would need to press the d-pad twice
|
||||
// due to the first press being caught by the "if (firstEvent)" case further up.
|
||||
else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) {
|
||||
numMovedAxis++;
|
||||
lastMovedRange = range;
|
||||
lastMovedDir = previousValue < 0.0f ? '-' : '+';
|
||||
}
|
||||
}
|
||||
|
||||
mPreviousValues.set(i, value);
|
||||
}
|
||||
|
||||
// If only one axis moved, that's the winner.
|
||||
if (numMovedAxis == 1) {
|
||||
mWaitingForEvent = false;
|
||||
setting.onMotionInput(input, lastMovedRange, lastMovedDir);
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
public final class BooleanSetting extends Setting {
|
||||
private boolean mValue;
|
||||
|
||||
public BooleanSetting(String key, String section, boolean value) {
|
||||
super(key, section);
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public boolean getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(boolean value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return mValue ? "True" : "False";
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
public final class FloatSetting extends Setting {
|
||||
private float mValue;
|
||||
|
||||
public FloatSetting(String key, String section, float value) {
|
||||
super(key, section);
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public float getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(float value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return Float.toString(mValue);
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
public final class IntSetting extends Setting {
|
||||
private int mValue;
|
||||
|
||||
public IntSetting(String key, String section, int value) {
|
||||
super(key, section);
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(int value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return Integer.toString(mValue);
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
/**
|
||||
* Abstraction for a setting item as read from / written to Citra's configuration ini files.
|
||||
* These files generally consist of a key/value pair, though the type of value is ambiguous and
|
||||
* must be inferred at read-time. The type of value determines which child of this class is used
|
||||
* to represent the Setting.
|
||||
*/
|
||||
public abstract class Setting {
|
||||
private String mKey;
|
||||
private String mSection;
|
||||
|
||||
/**
|
||||
* Base constructor.
|
||||
*
|
||||
* @param key Everything to the left of the = in a line from the ini file.
|
||||
* @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets.
|
||||
*/
|
||||
public Setting(String key, String section) {
|
||||
mKey = key;
|
||||
mSection = section;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The identifier used to write this setting to the ini file.
|
||||
*/
|
||||
public String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The name of the header under which this Setting should be written in the ini file.
|
||||
*/
|
||||
public String getSection() {
|
||||
return mSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A representation of this Setting's backing value converted to a String (e.g. for serialization).
|
||||
*/
|
||||
public abstract String getValueAsString();
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A semantically-related group of Settings objects. These Settings are
|
||||
* internally stored as a HashMap.
|
||||
*/
|
||||
public final class SettingSection {
|
||||
private String mName;
|
||||
|
||||
private HashMap<String, Setting> mSettings = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Create a new SettingSection with no Settings in it.
|
||||
*
|
||||
* @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets.
|
||||
*/
|
||||
public SettingSection(String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method; inserts a value directly into the backing HashMap.
|
||||
*
|
||||
* @param setting The Setting to be inserted.
|
||||
*/
|
||||
public void putSetting(Setting setting) {
|
||||
mSettings.put(setting.getKey(), setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method; gets a value directly from the backing HashMap.
|
||||
*
|
||||
* @param key Used to retrieve the Setting.
|
||||
* @return A Setting object (you should probably cast this before using)
|
||||
*/
|
||||
public Setting getSetting(String key) {
|
||||
return mSettings.get(key);
|
||||
}
|
||||
|
||||
public HashMap<String, Setting> getSettings() {
|
||||
return mSettings;
|
||||
}
|
||||
|
||||
public void mergeSection(SettingSection settingSection) {
|
||||
for (Setting setting : settingSection.mSettings.values()) {
|
||||
putSetting(setting);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class Settings {
|
||||
public static final String SECTION_PREMIUM = "Premium";
|
||||
public static final String SECTION_CORE = "Core";
|
||||
public static final String SECTION_SYSTEM = "System";
|
||||
public static final String SECTION_CAMERA = "Camera";
|
||||
public static final String SECTION_CONTROLS = "Controls";
|
||||
public static final String SECTION_RENDERER = "Renderer";
|
||||
public static final String SECTION_LAYOUT = "Layout";
|
||||
public static final String SECTION_UTILITY = "Utility";
|
||||
public static final String SECTION_AUDIO = "Audio";
|
||||
public static final String SECTION_DEBUG = "Debug";
|
||||
|
||||
private String gameId;
|
||||
|
||||
private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_PREMIUM, SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG));
|
||||
}
|
||||
|
||||
/**
|
||||
* A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null
|
||||
* when getting a key not already in the map
|
||||
*/
|
||||
public static final class SettingsSectionMap extends HashMap<String, SettingSection> {
|
||||
@Override
|
||||
public SettingSection get(Object key) {
|
||||
if (!(key instanceof String)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String stringKey = (String) key;
|
||||
|
||||
if (!super.containsKey(stringKey)) {
|
||||
SettingSection section = new SettingSection(stringKey);
|
||||
super.put(stringKey, section);
|
||||
return section;
|
||||
}
|
||||
return super.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
|
||||
|
||||
public SettingSection getSection(String sectionName) {
|
||||
return sections.get(sectionName);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return sections.isEmpty();
|
||||
}
|
||||
|
||||
public HashMap<String, SettingSection> getSections() {
|
||||
return sections;
|
||||
}
|
||||
|
||||
public void loadSettings(SettingsActivityView view) {
|
||||
sections = new Settings.SettingsSectionMap();
|
||||
loadCitraSettings(view);
|
||||
|
||||
if (!TextUtils.isEmpty(gameId)) {
|
||||
loadCustomGameSettings(gameId, view);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCitraSettings(SettingsActivityView view) {
|
||||
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
|
||||
String fileName = entry.getKey();
|
||||
sections.putAll(SettingsFile.readFile(fileName, view));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCustomGameSettings(String gameId, SettingsActivityView view) {
|
||||
// custom game settings
|
||||
mergeSections(SettingsFile.readCustomGameSettings(gameId, view));
|
||||
}
|
||||
|
||||
private void mergeSections(HashMap<String, SettingSection> updatedSections) {
|
||||
for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) {
|
||||
if (sections.containsKey(entry.getKey())) {
|
||||
SettingSection originalSection = sections.get(entry.getKey());
|
||||
SettingSection updatedSection = entry.getValue();
|
||||
originalSection.mergeSection(updatedSection);
|
||||
} else {
|
||||
sections.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadSettings(String gameId, SettingsActivityView view) {
|
||||
this.gameId = gameId;
|
||||
loadSettings(view);
|
||||
}
|
||||
|
||||
public void saveSettings(SettingsActivityView view) {
|
||||
if (TextUtils.isEmpty(gameId)) {
|
||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.ini_saved), false);
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
|
||||
String fileName = entry.getKey();
|
||||
List<String> sectionNames = entry.getValue();
|
||||
TreeMap<String, SettingSection> iniSections = new TreeMap<>();
|
||||
for (String section : sectionNames) {
|
||||
iniSections.put(section, sections.get(section));
|
||||
}
|
||||
|
||||
SettingsFile.saveFile(fileName, iniSections, view);
|
||||
}
|
||||
} else {
|
||||
// custom game settings
|
||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.gameid_saved, gameId), false);
|
||||
|
||||
SettingsFile.saveCustomGameSettings(gameId, sections);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model;
|
||||
|
||||
public final class StringSetting extends Setting {
|
||||
private String mValue;
|
||||
|
||||
public StringSetting(String key, String section, String value) {
|
||||
super(key, section);
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueAsString() {
|
||||
return mValue;
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting;
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
|
||||
|
||||
public final class CheckBoxSetting extends SettingsItem {
|
||||
private boolean mDefaultValue;
|
||||
private boolean mShowPerformanceWarning;
|
||||
private SettingsFragmentView mView;
|
||||
|
||||
public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
|
||||
boolean defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mDefaultValue = defaultValue;
|
||||
mShowPerformanceWarning = false;
|
||||
}
|
||||
|
||||
public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
|
||||
boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mDefaultValue = defaultValue;
|
||||
mView = view;
|
||||
mShowPerformanceWarning = show_performance_warning;
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
if (getSetting() == null) {
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
// Try integer setting
|
||||
try {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
return setting.getValue() == 1;
|
||||
} catch (ClassCastException exception) {
|
||||
}
|
||||
|
||||
// Try boolean setting
|
||||
try {
|
||||
BooleanSetting setting = (BooleanSetting) getSetting();
|
||||
return setting.getValue() == true;
|
||||
} catch (ClassCastException exception) {
|
||||
}
|
||||
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing boolean. If that boolean was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param checked Pretty self explanatory.
|
||||
* @return null if overwritten successfully; otherwise, a newly created BooleanSetting.
|
||||
*/
|
||||
public IntSetting setChecked(boolean checked) {
|
||||
// Show a performance warning if the setting has been disabled
|
||||
if (mShowPerformanceWarning && !checked) {
|
||||
mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.performance_warning), true);
|
||||
}
|
||||
|
||||
if (getSetting() == null) {
|
||||
IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
setting.setValue(checked ? 1 : 0);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_CHECKBOX;
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
|
||||
public final class DateTimeSetting extends SettingsItem {
|
||||
private String mDefaultValue;
|
||||
|
||||
public DateTimeSetting(String key, String section, int titleId, int descriptionId,
|
||||
String defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
if (getSetting() != null) {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
return setting.getValue();
|
||||
} else {
|
||||
return mDefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public StringSetting setSelectedValue(String datetime) {
|
||||
if (getSetting() == null) {
|
||||
StringSetting setting = new StringSetting(getKey(), getSection(), datetime);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
setting.setValue(datetime);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_DATETIME_SETTING;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class HeaderSetting extends SettingsItem {
|
||||
public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) {
|
||||
super(key, null, setting, titleId, descriptionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return SettingsItem.TYPE_HEADER;
|
||||
}
|
||||
}
|
@ -1,382 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
|
||||
public final class InputBindingSetting extends SettingsItem {
|
||||
private static final String INPUT_MAPPING_PREFIX = "InputMapping";
|
||||
|
||||
public InputBindingSetting(String key, String section, int titleId, Setting setting) {
|
||||
super(key, section, setting, titleId, 0);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
if (getSetting() == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
return setting.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS Circle Pad
|
||||
*/
|
||||
private boolean IsCirclePad() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
|
||||
*/
|
||||
public boolean IsHorizontalOrientation() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS C-Stick
|
||||
*/
|
||||
private boolean IsCStick() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_CSTICK_AXIS_VERTICAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS D-Pad
|
||||
*/
|
||||
private boolean IsDPad() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
|
||||
case SettingsFile.KEY_DPAD_AXIS_VERTICAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
|
||||
* triggers on the 3DS, but we support them as such on a physical gamepad.
|
||||
*/
|
||||
public boolean IsTrigger() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_BUTTON_L:
|
||||
case SettingsFile.KEY_BUTTON_R:
|
||||
case SettingsFile.KEY_BUTTON_ZL:
|
||||
case SettingsFile.KEY_BUTTON_ZR:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gamepad axis can be used to map this key.
|
||||
*/
|
||||
public boolean IsAxisMappingSupported() {
|
||||
return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gamepad button can be used to map this key.
|
||||
*/
|
||||
private boolean IsButtonMappingSupported() {
|
||||
return !IsAxisMappingSupported() || IsTrigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Citra button code for the settings key.
|
||||
*/
|
||||
private int getButtonCode() {
|
||||
switch (getKey()) {
|
||||
case SettingsFile.KEY_BUTTON_A:
|
||||
return NativeLibrary.ButtonType.BUTTON_A;
|
||||
case SettingsFile.KEY_BUTTON_B:
|
||||
return NativeLibrary.ButtonType.BUTTON_B;
|
||||
case SettingsFile.KEY_BUTTON_X:
|
||||
return NativeLibrary.ButtonType.BUTTON_X;
|
||||
case SettingsFile.KEY_BUTTON_Y:
|
||||
return NativeLibrary.ButtonType.BUTTON_Y;
|
||||
case SettingsFile.KEY_BUTTON_L:
|
||||
return NativeLibrary.ButtonType.TRIGGER_L;
|
||||
case SettingsFile.KEY_BUTTON_R:
|
||||
return NativeLibrary.ButtonType.TRIGGER_R;
|
||||
case SettingsFile.KEY_BUTTON_ZL:
|
||||
return NativeLibrary.ButtonType.BUTTON_ZL;
|
||||
case SettingsFile.KEY_BUTTON_ZR:
|
||||
return NativeLibrary.ButtonType.BUTTON_ZR;
|
||||
case SettingsFile.KEY_BUTTON_SELECT:
|
||||
return NativeLibrary.ButtonType.BUTTON_SELECT;
|
||||
case SettingsFile.KEY_BUTTON_START:
|
||||
return NativeLibrary.ButtonType.BUTTON_START;
|
||||
case SettingsFile.KEY_BUTTON_UP:
|
||||
return NativeLibrary.ButtonType.DPAD_UP;
|
||||
case SettingsFile.KEY_BUTTON_DOWN:
|
||||
return NativeLibrary.ButtonType.DPAD_DOWN;
|
||||
case SettingsFile.KEY_BUTTON_LEFT:
|
||||
return NativeLibrary.ButtonType.DPAD_LEFT;
|
||||
case SettingsFile.KEY_BUTTON_RIGHT:
|
||||
return NativeLibrary.ButtonType.DPAD_RIGHT;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the settings key for the specified Citra button code.
|
||||
*/
|
||||
private static String getButtonKey(int buttonCode) {
|
||||
switch (buttonCode) {
|
||||
case NativeLibrary.ButtonType.BUTTON_A:
|
||||
return SettingsFile.KEY_BUTTON_A;
|
||||
case NativeLibrary.ButtonType.BUTTON_B:
|
||||
return SettingsFile.KEY_BUTTON_B;
|
||||
case NativeLibrary.ButtonType.BUTTON_X:
|
||||
return SettingsFile.KEY_BUTTON_X;
|
||||
case NativeLibrary.ButtonType.BUTTON_Y:
|
||||
return SettingsFile.KEY_BUTTON_Y;
|
||||
case NativeLibrary.ButtonType.TRIGGER_L:
|
||||
return SettingsFile.KEY_BUTTON_L;
|
||||
case NativeLibrary.ButtonType.TRIGGER_R:
|
||||
return SettingsFile.KEY_BUTTON_R;
|
||||
case NativeLibrary.ButtonType.BUTTON_ZL:
|
||||
return SettingsFile.KEY_BUTTON_ZL;
|
||||
case NativeLibrary.ButtonType.BUTTON_ZR:
|
||||
return SettingsFile.KEY_BUTTON_ZR;
|
||||
case NativeLibrary.ButtonType.BUTTON_SELECT:
|
||||
return SettingsFile.KEY_BUTTON_SELECT;
|
||||
case NativeLibrary.ButtonType.BUTTON_START:
|
||||
return SettingsFile.KEY_BUTTON_START;
|
||||
case NativeLibrary.ButtonType.DPAD_UP:
|
||||
return SettingsFile.KEY_BUTTON_UP;
|
||||
case NativeLibrary.ButtonType.DPAD_DOWN:
|
||||
return SettingsFile.KEY_BUTTON_DOWN;
|
||||
case NativeLibrary.ButtonType.DPAD_LEFT:
|
||||
return SettingsFile.KEY_BUTTON_LEFT;
|
||||
case NativeLibrary.ButtonType.DPAD_RIGHT:
|
||||
return SettingsFile.KEY_BUTTON_RIGHT;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
|
||||
* settings on re-mapping or clearing of a setting.
|
||||
*/
|
||||
private String getReverseKey() {
|
||||
String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey();
|
||||
|
||||
if (IsAxisMappingSupported() && !IsTrigger()) {
|
||||
// Triggers are the only axis-supported mappings without orientation
|
||||
reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1);
|
||||
}
|
||||
|
||||
return reverseKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
|
||||
*/
|
||||
public void removeOldMapping() {
|
||||
// Get preferences editor
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
// Try remove all possible keys we wrote for this setting
|
||||
String oldKey = preferences.getString(getReverseKey(), "");
|
||||
if (!oldKey.equals("")) {
|
||||
editor.remove(getKey()); // Used for ui text
|
||||
editor.remove(oldKey); // Used for button mapping
|
||||
editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation
|
||||
editor.remove(oldKey + "_GuestButton"); // Used for axis button
|
||||
}
|
||||
|
||||
// Apply changes
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad button.
|
||||
*/
|
||||
public static String getInputButtonKey(int keyCode) {
|
||||
return INPUT_MAPPING_PREFIX + "_Button_" + keyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis.
|
||||
*/
|
||||
public static String getInputAxisKey(int axis) {
|
||||
return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis button (stick or trigger).
|
||||
*/
|
||||
public static String getInputAxisButtonKey(int axis) {
|
||||
return getInputAxisKey(axis) + "_GuestButton";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis orientation.
|
||||
*/
|
||||
public static String getInputAxisOrientationKey(int axis) {
|
||||
return getInputAxisKey(axis) + "_GuestOrientation";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to write a gamepad button mapping for the setting.
|
||||
*/
|
||||
private void WriteButtonMapping(String key) {
|
||||
// Get preferences editor
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
// Remove mapping for another setting using this input
|
||||
int oldButtonCode = preferences.getInt(key, -1);
|
||||
if (oldButtonCode != -1) {
|
||||
String oldKey = getButtonKey(oldButtonCode);
|
||||
editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten
|
||||
}
|
||||
|
||||
// Cleanup old mapping for this setting
|
||||
removeOldMapping();
|
||||
|
||||
// Write new mapping
|
||||
editor.putInt(key, getButtonCode());
|
||||
|
||||
// Write next reverse mapping for future cleanup
|
||||
editor.putString(getReverseKey(), key);
|
||||
|
||||
// Apply changes
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to write a gamepad axis mapping for the setting.
|
||||
*/
|
||||
private void WriteAxisMapping(int axis, int value) {
|
||||
// Get preferences editor
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
// Cleanup old mapping
|
||||
removeOldMapping();
|
||||
|
||||
// Write new mapping
|
||||
editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1);
|
||||
editor.putInt(getInputAxisButtonKey(axis), value);
|
||||
|
||||
// Write next reverse mapping for future cleanup
|
||||
editor.putString(getReverseKey(), getInputAxisKey(axis));
|
||||
|
||||
// Apply changes
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided key input setting as an Android preference.
|
||||
*
|
||||
* @param keyEvent KeyEvent of this key press.
|
||||
*/
|
||||
public void onKeyInput(KeyEvent keyEvent) {
|
||||
if (!IsButtonMappingSupported()) {
|
||||
Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
InputDevice device = keyEvent.getDevice();
|
||||
|
||||
WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode()));
|
||||
|
||||
String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
|
||||
setUiString(uiString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided motion input setting as an Android preference.
|
||||
*
|
||||
* @param device InputDevice from which the input event originated.
|
||||
* @param motionRange MotionRange of the movement
|
||||
* @param axisDir Either '-' or '+' (currently unused)
|
||||
*/
|
||||
public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
|
||||
char axisDir) {
|
||||
if (!IsAxisMappingSupported()) {
|
||||
Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
int button;
|
||||
if (IsCirclePad()) {
|
||||
button = NativeLibrary.ButtonType.STICK_LEFT;
|
||||
} else if (IsCStick()) {
|
||||
button = NativeLibrary.ButtonType.STICK_C;
|
||||
} else if (IsDPad()) {
|
||||
button = NativeLibrary.ButtonType.DPAD;
|
||||
} else {
|
||||
button = getButtonCode();
|
||||
}
|
||||
|
||||
WriteAxisMapping(motionRange.getAxis(), button);
|
||||
|
||||
String uiString = device.getName() + ": Axis " + motionRange.getAxis();
|
||||
setUiString(uiString);
|
||||
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string to use in the configuration UI for the gamepad input.
|
||||
*/
|
||||
private StringSetting setUiString(String ui) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
||||
if (getSetting() == null) {
|
||||
StringSetting setting = new StringSetting(getKey(), getSection(), "");
|
||||
setSetting(setting);
|
||||
|
||||
editor.putString(setting.getKey(), ui);
|
||||
editor.apply();
|
||||
|
||||
return setting;
|
||||
} else {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
|
||||
editor.putString(setting.getKey(), ui);
|
||||
editor.apply();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_INPUT_BINDING;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class PremiumHeader extends SettingsItem {
|
||||
public PremiumHeader() {
|
||||
super(null, null, null, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return SettingsItem.TYPE_PREMIUM;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
|
||||
|
||||
public final class PremiumSingleChoiceSetting extends SettingsItem {
|
||||
private int mDefaultValue;
|
||||
|
||||
private int mChoicesId;
|
||||
private int mValuesId;
|
||||
private SettingsFragmentView mView;
|
||||
|
||||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
|
||||
public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
|
||||
int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mValuesId = valuesId;
|
||||
mChoicesId = choicesId;
|
||||
mDefaultValue = defaultValue;
|
||||
mView = view;
|
||||
}
|
||||
|
||||
public int getChoicesId() {
|
||||
return mChoicesId;
|
||||
}
|
||||
|
||||
public int getValuesId() {
|
||||
return mValuesId;
|
||||
}
|
||||
|
||||
public int getSelectedValue() {
|
||||
return mPreferences.getInt(getKey(), mDefaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
||||
*/
|
||||
public void setSelectedValue(int selection) {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putInt(getKey(), selection);
|
||||
editor.apply();
|
||||
mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.design_updated), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_SINGLE_CHOICE;
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
/**
|
||||
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
|
||||
* Each one corresponds to a {@link Setting} object, so this class's subclasses
|
||||
* should vaguely correspond to those subclasses. There are a few with multiple analogues
|
||||
* and a few with none (Headers, for example, do not correspond to anything in the ini
|
||||
* file.)
|
||||
*/
|
||||
public abstract class SettingsItem {
|
||||
public static final int TYPE_HEADER = 0;
|
||||
public static final int TYPE_CHECKBOX = 1;
|
||||
public static final int TYPE_SINGLE_CHOICE = 2;
|
||||
public static final int TYPE_SLIDER = 3;
|
||||
public static final int TYPE_SUBMENU = 4;
|
||||
public static final int TYPE_INPUT_BINDING = 5;
|
||||
public static final int TYPE_STRING_SINGLE_CHOICE = 6;
|
||||
public static final int TYPE_DATETIME_SETTING = 7;
|
||||
public static final int TYPE_PREMIUM = 8;
|
||||
|
||||
private String mKey;
|
||||
private String mSection;
|
||||
|
||||
private Setting mSetting;
|
||||
|
||||
private int mNameId;
|
||||
private int mDescriptionId;
|
||||
private boolean mIsPremium;
|
||||
|
||||
/**
|
||||
* Base constructor. Takes a key / section name in case the third parameter, the Setting,
|
||||
* is null; in which case, one can be constructed and saved using the key / section.
|
||||
*
|
||||
* @param key Identifier for the Setting represented by this Item.
|
||||
* @param section Section to which the Setting belongs.
|
||||
* @param setting A possibly-null backing Setting, to be modified on UI events.
|
||||
* @param nameId Resource ID for a text string to be displayed as this setting's name.
|
||||
* @param descriptionId Resource ID for a text string to be displayed as this setting's description.
|
||||
*/
|
||||
public SettingsItem(String key, String section, Setting setting, int nameId,
|
||||
int descriptionId) {
|
||||
mKey = key;
|
||||
mSection = section;
|
||||
mSetting = setting;
|
||||
mNameId = nameId;
|
||||
mDescriptionId = descriptionId;
|
||||
mIsPremium = (section == Settings.SECTION_PREMIUM);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The identifier for the backing Setting.
|
||||
*/
|
||||
public String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The header under which the backing Setting belongs.
|
||||
*/
|
||||
public String getSection() {
|
||||
return mSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The backing Setting, possibly null.
|
||||
*/
|
||||
public Setting getSetting() {
|
||||
return mSetting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the backing setting with a new one. Generally used in cases where
|
||||
* the backing setting is null.
|
||||
*
|
||||
* @param setting A non-null Setting.
|
||||
*/
|
||||
public void setSetting(Setting setting) {
|
||||
mSetting = setting;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A resource ID for a text string representing this Setting's name.
|
||||
*/
|
||||
public int getNameId() {
|
||||
return mNameId;
|
||||
}
|
||||
|
||||
public int getDescriptionId() {
|
||||
return mDescriptionId;
|
||||
}
|
||||
|
||||
public boolean isPremium() {
|
||||
return mIsPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by {@link SettingsAdapter}'s onCreateViewHolder()
|
||||
* method to determine which type of ViewHolder should be created.
|
||||
*
|
||||
* @return An integer (ideally, one of the constants defined in this file)
|
||||
*/
|
||||
public abstract int getType();
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class SingleChoiceSetting extends SettingsItem {
|
||||
private int mDefaultValue;
|
||||
|
||||
private int mChoicesId;
|
||||
private int mValuesId;
|
||||
|
||||
public SingleChoiceSetting(String key, String section, int titleId, int descriptionId,
|
||||
int choicesId, int valuesId, int defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mValuesId = valuesId;
|
||||
mChoicesId = choicesId;
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public int getChoicesId() {
|
||||
return mChoicesId;
|
||||
}
|
||||
|
||||
public int getValuesId() {
|
||||
return mValuesId;
|
||||
}
|
||||
|
||||
public int getSelectedValue() {
|
||||
if (getSetting() != null) {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
return setting.getValue();
|
||||
} else {
|
||||
return mDefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
||||
*/
|
||||
public IntSetting setSelectedValue(int selection) {
|
||||
if (getSetting() == null) {
|
||||
IntSetting setting = new IntSetting(getKey(), getSection(), selection);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
setting.setValue(selection);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_SINGLE_CHOICE;
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.FloatSetting;
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
public final class SliderSetting extends SettingsItem {
|
||||
private int mMin;
|
||||
private int mMax;
|
||||
private int mDefaultValue;
|
||||
|
||||
private String mUnits;
|
||||
|
||||
public SliderSetting(String key, String section, int titleId, int descriptionId,
|
||||
int min, int max, String units, int defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mMin = min;
|
||||
mMax = max;
|
||||
mUnits = units;
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public int getMin() {
|
||||
return mMin;
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return mMax;
|
||||
}
|
||||
|
||||
public int getDefaultValue() {
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
public int getSelectedValue() {
|
||||
Setting setting = getSetting();
|
||||
|
||||
if (setting == null) {
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
if (setting instanceof IntSetting) {
|
||||
IntSetting intSetting = (IntSetting) setting;
|
||||
return intSetting.getValue();
|
||||
} else if (setting instanceof FloatSetting) {
|
||||
FloatSetting floatSetting = (FloatSetting) setting;
|
||||
return Math.round(floatSetting.getValue());
|
||||
} else {
|
||||
Log.error("[SliderSetting] Error casting setting type.");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
||||
*/
|
||||
public IntSetting setSelectedValue(int selection) {
|
||||
if (getSetting() == null) {
|
||||
IntSetting setting = new IntSetting(getKey(), getSection(), selection);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
IntSetting setting = (IntSetting) getSetting();
|
||||
setting.setValue(selection);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing float. If that float was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the float.
|
||||
* @return null if overwritten successfully otherwise; a newly created FloatSetting.
|
||||
*/
|
||||
public FloatSetting setSelectedValue(float selection) {
|
||||
if (getSetting() == null) {
|
||||
FloatSetting setting = new FloatSetting(getKey(), getSection(), selection);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
FloatSetting setting = (FloatSetting) getSetting();
|
||||
setting.setValue(selection);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getUnits() {
|
||||
return mUnits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_SLIDER;
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
|
||||
public class StringSingleChoiceSetting extends SettingsItem {
|
||||
private String mDefaultValue;
|
||||
|
||||
private String[] mChoicesId;
|
||||
private String[] mValuesId;
|
||||
|
||||
public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
|
||||
String[] choicesId, String[] valuesId, String defaultValue, Setting setting) {
|
||||
super(key, section, setting, titleId, descriptionId);
|
||||
mValuesId = valuesId;
|
||||
mChoicesId = choicesId;
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public String[] getChoicesId() {
|
||||
return mChoicesId;
|
||||
}
|
||||
|
||||
public String[] getValuesId() {
|
||||
return mValuesId;
|
||||
}
|
||||
|
||||
public String getValueAt(int index) {
|
||||
if (mValuesId == null)
|
||||
return null;
|
||||
|
||||
if (index >= 0 && index < mValuesId.length) {
|
||||
return mValuesId[index];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getSelectedValue() {
|
||||
if (getSetting() != null) {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
return setting.getValue();
|
||||
} else {
|
||||
return mDefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public int getSelectValueIndex() {
|
||||
String selectedValue = getSelectedValue();
|
||||
for (int i = 0; i < mValuesId.length; i++) {
|
||||
if (mValuesId[i].equals(selectedValue)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
*
|
||||
* @param selection New value of the int.
|
||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
||||
*/
|
||||
public StringSetting setSelectedValue(String selection) {
|
||||
if (getSetting() == null) {
|
||||
StringSetting setting = new StringSetting(getKey(), getSection(), selection);
|
||||
setSetting(setting);
|
||||
return setting;
|
||||
} else {
|
||||
StringSetting setting = (StringSetting) getSetting();
|
||||
setting.setValue(selection);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_STRING_SINGLE_CHOICE;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class SubmenuSetting extends SettingsItem {
|
||||
private String mMenuKey;
|
||||
|
||||
public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) {
|
||||
super(key, null, setting, titleId, descriptionId);
|
||||
mMenuKey = menuKey;
|
||||
}
|
||||
|
||||
public String getMenuKey() {
|
||||
return mMenuKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return TYPE_SUBMENU;
|
||||
}
|
||||
}
|
@ -1,242 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.InsetsHelper;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView {
|
||||
private static final String ARG_MENU_TAG = "menu_tag";
|
||||
private static final String ARG_GAME_ID = "game_id";
|
||||
private static final String FRAGMENT_TAG = "settings";
|
||||
private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this);
|
||||
|
||||
private ProgressDialog dialog;
|
||||
|
||||
public static void launch(Context context, String menuTag, String gameId) {
|
||||
Intent settings = new Intent(context, SettingsActivity.class);
|
||||
settings.putExtra(ARG_MENU_TAG, menuTag);
|
||||
settings.putExtra(ARG_GAME_ID, gameId);
|
||||
context.startActivity(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
ThemeUtil.applyTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
Intent launcher = getIntent();
|
||||
String gameID = launcher.getStringExtra(ARG_GAME_ID);
|
||||
String menuTag = launcher.getStringExtra(ARG_MENU_TAG);
|
||||
|
||||
mPresenter.onCreate(savedInstanceState, menuTag, gameID);
|
||||
|
||||
// Show "Back" button in the action bar for navigation
|
||||
MaterialToolbar toolbar = findViewById(R.id.toolbar_settings);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
setInsets();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_settings, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
// Critical: If super method is not called, rotations will be busted.
|
||||
super.onSaveInstanceState(outState);
|
||||
mPresenter.saveState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
mPresenter.onStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is called, the user has left the settings screen (potentially through the
|
||||
* home button) and will expect their changes to be persisted. So we kick off an
|
||||
* IntentService which will do so on a background thread.
|
||||
*/
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
mPresenter.onStop(isFinishing());
|
||||
|
||||
// Update framebuffer layout when closing the settings
|
||||
NativeLibrary.NotifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(),
|
||||
getWindowManager().getDefaultDisplay().getRotation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) {
|
||||
if (!addToStack && getFragment() != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
|
||||
if (addToStack) {
|
||||
if (areSystemAnimationsEnabled()) {
|
||||
transaction.setCustomAnimations(
|
||||
R.anim.anim_settings_fragment_in,
|
||||
R.anim.anim_settings_fragment_out,
|
||||
0,
|
||||
R.anim.anim_pop_settings_fragment_out);
|
||||
}
|
||||
|
||||
transaction.addToBackStack(null);
|
||||
}
|
||||
transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG);
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
private boolean areSystemAnimationsEnabled() {
|
||||
float duration = Settings.Global.getFloat(
|
||||
getContentResolver(),
|
||||
Settings.Global.ANIMATOR_DURATION_SCALE, 1);
|
||||
float transition = Settings.Global.getFloat(
|
||||
getContentResolver(),
|
||||
Settings.Global.TRANSITION_ANIMATION_SCALE, 1);
|
||||
return duration != 0 && transition != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) {
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||
receiver,
|
||||
filter);
|
||||
DirectoryInitialization.start(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
if (dialog == null) {
|
||||
dialog = new ProgressDialog(this);
|
||||
dialog.setMessage(getString(R.string.load_settings));
|
||||
dialog.setIndeterminate(true);
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showPermissionNeededHint() {
|
||||
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showExternalStorageNotMountedHint() {
|
||||
Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.citra.citra_emu.features.settings.model.Settings getSettings() {
|
||||
return mPresenter.getSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) {
|
||||
mPresenter.setSettings(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) {
|
||||
SettingsFragmentView fragment = getFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
fragment.onSettingsFileLoaded(settings);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsFileNotFound() {
|
||||
SettingsFragmentView fragment = getFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
fragment.loadDefaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showToastMessage(String message, boolean is_long) {
|
||||
Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingChanged() {
|
||||
mPresenter.onSettingChanged();
|
||||
}
|
||||
|
||||
private SettingsFragment getFragment() {
|
||||
return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
private void setInsets() {
|
||||
AppBarLayout appBar = findViewById(R.id.appbar_settings);
|
||||
FrameLayout frame = findViewById(R.id.frame_content);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
InsetsHelper.insetAppBar(insets, appBar);
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import java.io.File;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
public final class SettingsActivityPresenter {
|
||||
private static final String KEY_SHOULD_SAVE = "should_save";
|
||||
|
||||
private SettingsActivityView mView;
|
||||
|
||||
private Settings mSettings = new Settings();
|
||||
|
||||
private boolean mShouldSave;
|
||||
|
||||
private DirectoryStateReceiver directoryStateReceiver;
|
||||
|
||||
private String menuTag;
|
||||
private String gameId;
|
||||
|
||||
public SettingsActivityPresenter(SettingsActivityView view) {
|
||||
mView = view;
|
||||
}
|
||||
|
||||
public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) {
|
||||
if (savedInstanceState == null) {
|
||||
this.menuTag = menuTag;
|
||||
this.gameId = gameId;
|
||||
} else {
|
||||
mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE);
|
||||
}
|
||||
}
|
||||
|
||||
public void onStart() {
|
||||
prepareCitraDirectoriesIfNeeded();
|
||||
}
|
||||
|
||||
void loadSettingsUI() {
|
||||
if (mSettings.isEmpty()) {
|
||||
if (!TextUtils.isEmpty(gameId)) {
|
||||
mSettings.loadSettings(gameId, mView);
|
||||
} else {
|
||||
mSettings.loadSettings(mView);
|
||||
}
|
||||
}
|
||||
|
||||
mView.showSettingsFragment(menuTag, false, gameId);
|
||||
mView.onSettingsFileLoaded(mSettings);
|
||||
}
|
||||
|
||||
private void prepareCitraDirectoriesIfNeeded() {
|
||||
DocumentFile configFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG);
|
||||
if (configFile == null || !configFile.exists()) {
|
||||
Log.error("Citra config file could not be found!");
|
||||
}
|
||||
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||
loadSettingsUI();
|
||||
} else {
|
||||
mView.showLoading();
|
||||
IntentFilter statusIntentFilter = new IntentFilter(
|
||||
DirectoryInitialization.BROADCAST_ACTION);
|
||||
|
||||
directoryStateReceiver =
|
||||
new DirectoryStateReceiver(directoryInitializationState ->
|
||||
{
|
||||
if (directoryInitializationState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
|
||||
mView.hideLoading();
|
||||
loadSettingsUI();
|
||||
} else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
|
||||
mView.showPermissionNeededHint();
|
||||
mView.hideLoading();
|
||||
} else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
||||
mView.showExternalStorageNotMountedHint();
|
||||
mView.hideLoading();
|
||||
}
|
||||
});
|
||||
|
||||
mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSettings(Settings settings) {
|
||||
mSettings = settings;
|
||||
}
|
||||
|
||||
public Settings getSettings() {
|
||||
return mSettings;
|
||||
}
|
||||
|
||||
public void onStop(boolean finishing) {
|
||||
if (directoryStateReceiver != null) {
|
||||
mView.stopListeningToDirectoryInitializationService(directoryStateReceiver);
|
||||
directoryStateReceiver = null;
|
||||
}
|
||||
|
||||
if (mSettings != null && finishing && mShouldSave) {
|
||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
|
||||
mSettings.saveSettings(mView);
|
||||
}
|
||||
|
||||
NativeLibrary.ReloadSettings();
|
||||
}
|
||||
|
||||
public void onSettingChanged() {
|
||||
mShouldSave = true;
|
||||
}
|
||||
|
||||
public void saveState(Bundle outState) {
|
||||
outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave);
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
||||
|
||||
/**
|
||||
* Abstraction for the Activity that manages SettingsFragments.
|
||||
*/
|
||||
public interface SettingsActivityView {
|
||||
/**
|
||||
* Show a new SettingsFragment.
|
||||
*
|
||||
* @param menuTag Identifier for the settings group that should be displayed.
|
||||
* @param addToStack Whether or not this fragment should replace a previous one.
|
||||
*/
|
||||
void showSettingsFragment(String menuTag, boolean addToStack, String gameId);
|
||||
|
||||
/**
|
||||
* Called by a contained Fragment to get access to the Setting HashMap
|
||||
* loaded from disk, so that each Fragment doesn't need to perform its own
|
||||
* read operation.
|
||||
*
|
||||
* @return A possibly null HashMap of Settings.
|
||||
*/
|
||||
Settings getSettings();
|
||||
|
||||
/**
|
||||
* Used to provide the Activity with Settings HashMaps if a Fragment already
|
||||
* has one; for example, if a rotation occurs, the Fragment will not be killed,
|
||||
* but the Activity will, so the Activity needs to have its HashMaps resupplied.
|
||||
*
|
||||
* @param settings The ArrayList of all the Settings HashMaps.
|
||||
*/
|
||||
void setSettings(Settings settings);
|
||||
|
||||
/**
|
||||
* Called when an asynchronous load operation completes.
|
||||
*
|
||||
* @param settings The (possibly null) result of the ini load operation.
|
||||
*/
|
||||
void onSettingsFileLoaded(Settings settings);
|
||||
|
||||
/**
|
||||
* Called when an asynchronous load operation fails.
|
||||
*/
|
||||
void onSettingsFileNotFound();
|
||||
|
||||
/**
|
||||
* Display a popup text message on screen.
|
||||
*
|
||||
* @param message The contents of the onscreen message.
|
||||
* @param is_long Whether this should be a long Toast or short one.
|
||||
*/
|
||||
void showToastMessage(String message, boolean is_long);
|
||||
|
||||
/**
|
||||
* End the activity.
|
||||
*/
|
||||
void finish();
|
||||
|
||||
/**
|
||||
* Called by a containing Fragment to tell the Activity that a setting was changed;
|
||||
* unless this has been called, the Activity will not save to disk.
|
||||
*/
|
||||
void onSettingChanged();
|
||||
|
||||
/**
|
||||
* Show loading dialog while loading the settings
|
||||
*/
|
||||
void showLoading();
|
||||
|
||||
/**
|
||||
* Hide the loading the dialog
|
||||
*/
|
||||
void hideLoading();
|
||||
|
||||
/**
|
||||
* Show a hint to the user that the app needs write to external storage access
|
||||
*/
|
||||
void showPermissionNeededHint();
|
||||
|
||||
/**
|
||||
* Show a hint to the user that the app needs the external storage to be mounted
|
||||
*/
|
||||
void showExternalStorageNotMountedHint();
|
||||
|
||||
/**
|
||||
* Start the DirectoryInitialization and listen for the result.
|
||||
*
|
||||
* @param receiver the broadcast receiver for the DirectoryInitialization
|
||||
* @param filter the Intent broadcasts to be received.
|
||||
*/
|
||||
void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter);
|
||||
|
||||
/**
|
||||
* Stop listening to the DirectoryInitialization.
|
||||
*
|
||||
* @param receiver The broadcast receiver to unregister.
|
||||
*/
|
||||
void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver);
|
||||
}
|
@ -1,474 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TimePicker;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.slider.Slider;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.dialogs.MotionAlertDialog;
|
||||
import org.citra.citra_emu.features.settings.model.FloatSetting;
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> implements DialogInterface.OnClickListener, Slider.OnChangeListener {
|
||||
private SettingsFragmentView mView;
|
||||
private Context mContext;
|
||||
private ArrayList<SettingsItem> mSettings;
|
||||
|
||||
private SettingsItem mClickedItem;
|
||||
private int mClickedPosition;
|
||||
private int mSliderProgress;
|
||||
|
||||
private AlertDialog mDialog;
|
||||
private TextView mTextSliderValue;
|
||||
|
||||
public SettingsAdapter(SettingsFragmentView view, Context context) {
|
||||
mView = view;
|
||||
mContext = context;
|
||||
mClickedPosition = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view;
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
|
||||
switch (viewType) {
|
||||
case SettingsItem.TYPE_HEADER:
|
||||
view = inflater.inflate(R.layout.list_item_settings_header, parent, false);
|
||||
return new HeaderViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_CHECKBOX:
|
||||
view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false);
|
||||
return new CheckBoxSettingViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_SINGLE_CHOICE:
|
||||
case SettingsItem.TYPE_STRING_SINGLE_CHOICE:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new SingleChoiceViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_SLIDER:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new SliderViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_SUBMENU:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new SubmenuViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_INPUT_BINDING:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new InputBindingSettingViewHolder(view, this, mContext);
|
||||
|
||||
case SettingsItem.TYPE_DATETIME_SETTING:
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new DateTimeViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_PREMIUM:
|
||||
view = inflater.inflate(R.layout.premium_item_setting, parent, false);
|
||||
return new PremiumViewHolder(view, this, mView);
|
||||
|
||||
default:
|
||||
Log.error("[SettingsAdapter] Invalid view type: " + viewType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SettingViewHolder holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
private SettingsItem getItem(int position) {
|
||||
return mSettings.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (mSettings != null) {
|
||||
return mSettings.size();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return getItem(position).getType();
|
||||
}
|
||||
|
||||
public void setSettings(ArrayList<SettingsItem> settings) {
|
||||
mSettings = settings;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) {
|
||||
IntSetting setting = item.setChecked(checked);
|
||||
notifyItemChanged(position);
|
||||
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
|
||||
mView.onSettingChanged();
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(PremiumSingleChoiceSetting item) {
|
||||
mClickedItem = item;
|
||||
|
||||
int value = getSelectionForSingleChoiceValue(item);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setTitle(item.getNameId())
|
||||
.setSingleChoiceItems(item.getChoicesId(), value, this);
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(SingleChoiceSetting item) {
|
||||
mClickedItem = item;
|
||||
|
||||
int value = getSelectionForSingleChoiceValue(item);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setTitle(item.getNameId())
|
||||
.setSingleChoiceItems(item.getChoicesId(), value, this);
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
|
||||
mClickedPosition = position;
|
||||
|
||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
||||
// Setting is either not Premium, or the user has Premium
|
||||
onSingleChoiceClick(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// User needs Premium, invoke the billing flow
|
||||
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) {
|
||||
mClickedPosition = position;
|
||||
|
||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
||||
// Setting is either not Premium, or the user has Premium
|
||||
onSingleChoiceClick(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// User needs Premium, invoke the billing flow
|
||||
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
|
||||
}
|
||||
|
||||
public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
|
||||
mClickedItem = item;
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setTitle(item.getNameId())
|
||||
.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this);
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
|
||||
mClickedPosition = position;
|
||||
|
||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
||||
// Setting is either not Premium, or the user has Premium
|
||||
onStringSingleChoiceClick(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// User needs Premium, invoke the billing flow
|
||||
MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item));
|
||||
}
|
||||
|
||||
DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog();
|
||||
|
||||
public void onDateTimeClick(DateTimeSetting item, int position) {
|
||||
mClickedItem = item;
|
||||
mClickedPosition = position;
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
|
||||
View view = inflater.inflate(R.layout.sysclock_datetime_picker, null);
|
||||
|
||||
DatePicker dp = view.findViewById(R.id.date_picker);
|
||||
TimePicker tp = view.findViewById(R.id.time_picker);
|
||||
|
||||
//set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69)
|
||||
String settingValue = item.getValue();
|
||||
dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10)));
|
||||
|
||||
tp.setIs24HourView(true);
|
||||
tp.setHour(Integer.parseInt(settingValue.substring(11, 13)));
|
||||
tp.setMinute(Integer.parseInt(settingValue.substring(14, 16)));
|
||||
|
||||
DialogInterface.OnClickListener ok = (dialog, which) -> {
|
||||
//set it
|
||||
int year = dp.getYear();
|
||||
if (year < 2000) {
|
||||
year = 2000;
|
||||
}
|
||||
String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length());
|
||||
String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length());
|
||||
String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length());
|
||||
String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length());
|
||||
String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01";
|
||||
|
||||
StringSetting setting = item.setSelectedValue(datetime);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
|
||||
mView.onSettingChanged();
|
||||
|
||||
mClickedItem = null;
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, ok)
|
||||
.setNegativeButton(android.R.string.cancel, defaultCancelListener);
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onSliderClick(SliderSetting item, int position) {
|
||||
mClickedItem = item;
|
||||
mClickedPosition = position;
|
||||
mSliderProgress = item.getSelectedValue();
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
|
||||
View view = inflater.inflate(R.layout.dialog_slider, null);
|
||||
|
||||
Slider slider = view.findViewById(R.id.slider);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
||||
.setTitle(item.getNameId())
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
|
||||
.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> {
|
||||
slider.setValue(item.getDefaultValue());
|
||||
onClick(dialog, which);
|
||||
});
|
||||
mDialog = builder.show();
|
||||
|
||||
mTextSliderValue = view.findViewById(R.id.text_value);
|
||||
mTextSliderValue.setText(String.valueOf(mSliderProgress));
|
||||
|
||||
TextView units = view.findViewById(R.id.text_units);
|
||||
units.setText(item.getUnits());
|
||||
|
||||
slider.setValueFrom(item.getMin());
|
||||
slider.setValueTo(item.getMax());
|
||||
slider.setValue(mSliderProgress);
|
||||
|
||||
slider.addOnChangeListener(this);
|
||||
}
|
||||
|
||||
public void onSubmenuClick(SubmenuSetting item) {
|
||||
mView.loadSubMenu(item.getMenuKey());
|
||||
}
|
||||
|
||||
public void onInputBindingClick(final InputBindingSetting item, final int position) {
|
||||
final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item);
|
||||
dialog.setTitle(R.string.input_binding);
|
||||
|
||||
int messageResId = R.string.input_binding_description;
|
||||
if (item.IsAxisMappingSupported() && !item.IsTrigger()) {
|
||||
// Use specialized message for axis left/right or up/down
|
||||
if (item.IsHorizontalOrientation()) {
|
||||
messageResId = R.string.input_binding_description_horizontal_axis;
|
||||
} else {
|
||||
messageResId = R.string.input_binding_description_vertical_axis;
|
||||
}
|
||||
}
|
||||
|
||||
dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId())));
|
||||
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this);
|
||||
dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) ->
|
||||
item.removeOldMapping());
|
||||
dialog.setOnDismissListener(dialog1 ->
|
||||
{
|
||||
StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue());
|
||||
notifyItemChanged(position);
|
||||
|
||||
mView.putSetting(setting);
|
||||
|
||||
mView.onSettingChanged();
|
||||
});
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (mClickedItem instanceof SingleChoiceSetting) {
|
||||
SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem;
|
||||
|
||||
int value = getValueForSingleChoiceSelection(scSetting, which);
|
||||
if (scSetting.getSelectedValue() != value) {
|
||||
mView.onSettingChanged();
|
||||
}
|
||||
|
||||
// Get the backing Setting, which may be null (if for example it was missing from the file)
|
||||
IntSetting setting = scSetting.setSelectedValue(value);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
} else if (mClickedItem instanceof PremiumSingleChoiceSetting) {
|
||||
PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem;
|
||||
scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which));
|
||||
closeDialog();
|
||||
} else if (mClickedItem instanceof StringSingleChoiceSetting) {
|
||||
StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem;
|
||||
String value = scSetting.getValueAt(which);
|
||||
if (!scSetting.getSelectedValue().equals(value))
|
||||
mView.onSettingChanged();
|
||||
|
||||
StringSetting setting = scSetting.setSelectedValue(value);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
} else if (mClickedItem instanceof SliderSetting) {
|
||||
SliderSetting sliderSetting = (SliderSetting) mClickedItem;
|
||||
if (sliderSetting.getSelectedValue() != mSliderProgress) {
|
||||
mView.onSettingChanged();
|
||||
}
|
||||
|
||||
if (sliderSetting.getSetting() instanceof FloatSetting) {
|
||||
float value = (float) mSliderProgress;
|
||||
|
||||
FloatSetting setting = sliderSetting.setSelectedValue(value);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
} else {
|
||||
IntSetting setting = sliderSetting.setSelectedValue(mSliderProgress);
|
||||
if (setting != null) {
|
||||
mView.putSetting(setting);
|
||||
}
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
mClickedItem = null;
|
||||
mSliderProgress = -1;
|
||||
}
|
||||
|
||||
public void closeDialog() {
|
||||
if (mDialog != null) {
|
||||
if (mClickedPosition != -1) {
|
||||
notifyItemChanged(mClickedPosition);
|
||||
mClickedPosition = -1;
|
||||
}
|
||||
mDialog.dismiss();
|
||||
mDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) {
|
||||
int valuesId = item.getValuesId();
|
||||
|
||||
if (valuesId > 0) {
|
||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
||||
return valuesArray[which];
|
||||
} else {
|
||||
return which;
|
||||
}
|
||||
}
|
||||
|
||||
private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) {
|
||||
int valuesId = item.getValuesId();
|
||||
|
||||
if (valuesId > 0) {
|
||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
||||
return valuesArray[which];
|
||||
} else {
|
||||
return which;
|
||||
}
|
||||
}
|
||||
|
||||
private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) {
|
||||
int value = item.getSelectedValue();
|
||||
int valuesId = item.getValuesId();
|
||||
|
||||
if (valuesId > 0) {
|
||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
||||
for (int index = 0; index < valuesArray.length; index++) {
|
||||
int current = valuesArray[index];
|
||||
if (current == value) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) {
|
||||
int value = item.getSelectedValue();
|
||||
int valuesId = item.getValuesId();
|
||||
|
||||
if (valuesId > 0) {
|
||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
||||
for (int index = 0; index < valuesArray.length; index++) {
|
||||
int current = valuesArray[index];
|
||||
if (current == value) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
|
||||
mSliderProgress = (int) value;
|
||||
mTextSliderValue.setText(String.valueOf(mSliderProgress));
|
||||
}
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.ui.DividerItemDecoration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class SettingsFragment extends Fragment implements SettingsFragmentView {
|
||||
private static final String ARGUMENT_MENU_TAG = "menu_tag";
|
||||
private static final String ARGUMENT_GAME_ID = "game_id";
|
||||
|
||||
private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this);
|
||||
private SettingsActivityView mActivity;
|
||||
|
||||
private SettingsAdapter mAdapter;
|
||||
|
||||
private RecyclerView mRecyclerView;
|
||||
|
||||
public static Fragment newInstance(String menuTag, String gameId) {
|
||||
SettingsFragment fragment = new SettingsFragment();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString(ARGUMENT_MENU_TAG, menuTag);
|
||||
arguments.putString(ARGUMENT_GAME_ID, gameId);
|
||||
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
mActivity = (SettingsActivityView) context;
|
||||
mPresenter.onAttach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setRetainInstance(true);
|
||||
String menuTag = getArguments().getString(ARGUMENT_MENU_TAG);
|
||||
String gameId = getArguments().getString(ARGUMENT_GAME_ID);
|
||||
|
||||
mAdapter = new SettingsAdapter(this, getActivity());
|
||||
|
||||
mPresenter.onCreate(menuTag, gameId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_settings, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
LinearLayoutManager manager = new LinearLayoutManager(getActivity());
|
||||
|
||||
mRecyclerView = view.findViewById(R.id.list_settings);
|
||||
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
mRecyclerView.setLayoutManager(manager);
|
||||
mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
|
||||
|
||||
SettingsActivityView activity = (SettingsActivityView) getActivity();
|
||||
|
||||
mPresenter.onViewCreated(activity.getSettings());
|
||||
|
||||
setInsets();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
mActivity = null;
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsFileLoaded(Settings settings) {
|
||||
mPresenter.setSettings(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void passSettingsToActivity(Settings settings) {
|
||||
if (mActivity != null) {
|
||||
mActivity.setSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSettingsList(ArrayList<SettingsItem> settingsList) {
|
||||
mAdapter.setSettings(settingsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadDefaultSettings() {
|
||||
mPresenter.loadDefaultSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSubMenu(String menuKey) {
|
||||
mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showToastMessage(String message, boolean is_long) {
|
||||
mActivity.showToastMessage(message, is_long);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putSetting(Setting setting) {
|
||||
mPresenter.putSetting(setting);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingChanged() {
|
||||
mActivity.onSettingChanged();
|
||||
}
|
||||
|
||||
private void setInsets() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(insets.left, 0, insets.right, insets.bottom);
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,433 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.hardware.camera2.CameraAccessException;
|
||||
import android.hardware.camera2.CameraCharacteristics;
|
||||
import android.hardware.camera2.CameraManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.SettingSection;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.HeaderSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.PremiumHeader;
|
||||
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SettingsFragmentPresenter {
|
||||
private SettingsFragmentView mView;
|
||||
|
||||
private String mMenuTag;
|
||||
private String mGameID;
|
||||
|
||||
private Settings mSettings;
|
||||
private ArrayList<SettingsItem> mSettingsList;
|
||||
|
||||
public SettingsFragmentPresenter(SettingsFragmentView view) {
|
||||
mView = view;
|
||||
}
|
||||
|
||||
public void onCreate(String menuTag, String gameId) {
|
||||
mGameID = gameId;
|
||||
mMenuTag = menuTag;
|
||||
}
|
||||
|
||||
public void onViewCreated(Settings settings) {
|
||||
setSettings(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the screen is rotated, the Activity will forget the settings map. This fragment
|
||||
* won't, though; so rather than have the Activity reload from disk, have the fragment pass
|
||||
* the settings map back to the Activity.
|
||||
*/
|
||||
public void onAttach() {
|
||||
if (mSettings != null) {
|
||||
mView.passSettingsToActivity(mSettings);
|
||||
}
|
||||
}
|
||||
|
||||
public void putSetting(Setting setting) {
|
||||
mSettings.getSection(setting.getSection()).putSetting(setting);
|
||||
}
|
||||
|
||||
private StringSetting asStringSetting(Setting setting) {
|
||||
if (setting == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString());
|
||||
putSetting(stringSetting);
|
||||
return stringSetting;
|
||||
}
|
||||
|
||||
public void loadDefaultSettings() {
|
||||
loadSettingsList();
|
||||
}
|
||||
|
||||
public void setSettings(Settings settings) {
|
||||
if (mSettingsList == null && settings != null) {
|
||||
mSettings = settings;
|
||||
|
||||
loadSettingsList();
|
||||
} else {
|
||||
mView.getActivity().setTitle(R.string.preferences_settings);
|
||||
mView.showSettingsList(mSettingsList);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSettingsList() {
|
||||
if (!TextUtils.isEmpty(mGameID)) {
|
||||
mView.getActivity().setTitle("Game Settings: " + mGameID);
|
||||
}
|
||||
ArrayList<SettingsItem> sl = new ArrayList<>();
|
||||
|
||||
if (mMenuTag == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mMenuTag) {
|
||||
case SettingsFile.FILE_NAME_CONFIG:
|
||||
addConfigSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_PREMIUM:
|
||||
addPremiumSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_CORE:
|
||||
addGeneralSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_SYSTEM:
|
||||
addSystemSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_CAMERA:
|
||||
addCameraSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_CONTROLS:
|
||||
addInputSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_RENDERER:
|
||||
addGraphicsSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_AUDIO:
|
||||
addAudioSettings(sl);
|
||||
break;
|
||||
case Settings.SECTION_DEBUG:
|
||||
addDebugSettings(sl);
|
||||
break;
|
||||
default:
|
||||
mView.showToastMessage("Unimplemented menu", false);
|
||||
return;
|
||||
}
|
||||
|
||||
mSettingsList = sl;
|
||||
mView.showSettingsList(mSettingsList);
|
||||
}
|
||||
|
||||
private void addConfigSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_settings);
|
||||
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO));
|
||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG));
|
||||
}
|
||||
|
||||
private void addPremiumSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_premium);
|
||||
|
||||
SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM);
|
||||
Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN);
|
||||
|
||||
sl.add(new PremiumHeader());
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView));
|
||||
} else {
|
||||
// Pre-Android 10 does not support System Default
|
||||
sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView));
|
||||
}
|
||||
|
||||
Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME);
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_PREMIUM, R.string.texture_filter_name, 0, R.array.textureFilterNames, R.array.textureFilterValues, 0, textureFilterName));
|
||||
}
|
||||
|
||||
private void addGeneralSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_general);
|
||||
|
||||
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
|
||||
Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED);
|
||||
Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT);
|
||||
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue));
|
||||
}
|
||||
|
||||
private void addSystemSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_system);
|
||||
|
||||
SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM);
|
||||
Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE);
|
||||
Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE);
|
||||
Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK);
|
||||
Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME);
|
||||
Setting pluginLoader = systemSection.getSetting(SettingsFile.KEY_PLUGIN_LOADER);
|
||||
Setting allowPluginLoader = systemSection.getSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER);
|
||||
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.clock, 0));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock));
|
||||
sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.plugin_loader, 0));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.plugin_loader, R.string.plugin_loader_description, false, pluginLoader));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.allow_plugin_loader, R.string.allow_plugin_loader_description, true, allowPluginLoader));
|
||||
}
|
||||
|
||||
private void addCameraSettings(ArrayList<SettingsItem> sl) {
|
||||
final Activity activity = mView.getActivity();
|
||||
activity.setTitle(R.string.preferences_camera);
|
||||
|
||||
// Get the camera IDs
|
||||
CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
|
||||
ArrayList<String> supportedCameraNameList = new ArrayList<>();
|
||||
ArrayList<String> supportedCameraIdList = new ArrayList<>();
|
||||
if (cameraManager != null) {
|
||||
try {
|
||||
for (String id : cameraManager.getCameraIdList()) {
|
||||
final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
|
||||
if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
|
||||
continue; // Legacy cameras cannot be used with the NDK
|
||||
}
|
||||
|
||||
supportedCameraIdList.add(id);
|
||||
|
||||
final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING));
|
||||
int stringId = R.string.camera_facing_external;
|
||||
switch (facing) {
|
||||
case CameraCharacteristics.LENS_FACING_FRONT:
|
||||
stringId = R.string.camera_facing_front;
|
||||
break;
|
||||
case CameraCharacteristics.LENS_FACING_BACK:
|
||||
stringId = R.string.camera_facing_back;
|
||||
break;
|
||||
case CameraCharacteristics.LENS_FACING_EXTERNAL:
|
||||
stringId = R.string.camera_facing_external;
|
||||
break;
|
||||
}
|
||||
supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId)));
|
||||
}
|
||||
} catch (CameraAccessException e) {
|
||||
Log.error("Couldn't retrieve camera list");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Create the names and values for display
|
||||
ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames)));
|
||||
cameraDeviceNameList.addAll(supportedCameraNameList);
|
||||
ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues)));
|
||||
cameraDeviceValueList.addAll(supportedCameraIdList);
|
||||
|
||||
final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{});
|
||||
final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{});
|
||||
|
||||
final boolean haveCameraDevices = !supportedCameraIdList.isEmpty();
|
||||
|
||||
String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames);
|
||||
String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues);
|
||||
if (!haveCameraDevices) {
|
||||
// Remove the last entry (ndk / Device Camera)
|
||||
imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1);
|
||||
imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1);
|
||||
}
|
||||
|
||||
final String defaultImageSource = haveCameraDevices ? "ndk" : "image";
|
||||
|
||||
SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA);
|
||||
|
||||
Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME);
|
||||
Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG));
|
||||
Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP);
|
||||
sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0));
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource));
|
||||
if (haveCameraDevices)
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip));
|
||||
|
||||
Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME);
|
||||
Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG));
|
||||
Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP);
|
||||
sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0));
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource));
|
||||
if (haveCameraDevices)
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip));
|
||||
|
||||
Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME);
|
||||
Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG));
|
||||
Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP);
|
||||
sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0));
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource));
|
||||
if (haveCameraDevices)
|
||||
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip));
|
||||
}
|
||||
|
||||
private void addInputSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_controls);
|
||||
|
||||
SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS);
|
||||
Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A);
|
||||
Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B);
|
||||
Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X);
|
||||
Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y);
|
||||
Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT);
|
||||
Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START);
|
||||
Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL);
|
||||
Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL);
|
||||
Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL);
|
||||
Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL);
|
||||
Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL);
|
||||
Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL);
|
||||
// Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP);
|
||||
// Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN);
|
||||
// Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT);
|
||||
// Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT);
|
||||
Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L);
|
||||
Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R);
|
||||
Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL);
|
||||
Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR);
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.controller_c, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz));
|
||||
|
||||
// TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing.
|
||||
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp));
|
||||
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown));
|
||||
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft));
|
||||
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL));
|
||||
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR));
|
||||
}
|
||||
|
||||
private void addGraphicsSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_graphics);
|
||||
|
||||
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
|
||||
Setting graphicsApi = rendererSection.getSetting(SettingsFile.KEY_GRAPHICS_API);
|
||||
Setting spirvShaderGen = rendererSection.getSetting(SettingsFile.KEY_SPIRV_SHADER_GEN);
|
||||
Setting asyncShaders = rendererSection.getSetting(SettingsFile.KEY_ASYNC_SHADERS);
|
||||
Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR);
|
||||
Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE);
|
||||
Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL);
|
||||
Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D);
|
||||
Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D);
|
||||
Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE);
|
||||
SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT);
|
||||
Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE);
|
||||
Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT);
|
||||
Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT);
|
||||
SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY);
|
||||
Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES);
|
||||
Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES);
|
||||
Setting asyncCustomLoading = utilitySection.getSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING);
|
||||
//Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES);
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.renderer, 0));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_GRAPHICS_API, Settings.SECTION_RENDERER, R.string.graphics_api, 0, R.array.graphicsApiNames, R.array.graphicsApiValues, 0, graphicsApi));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_SPIRV_SHADER_GEN, Settings.SECTION_RENDERER, R.string.spirv_shader_gen, R.string.spirv_shader_gen_description, true, spirvShaderGen));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_SHADERS, Settings.SECTION_RENDERER, R.string.async_shaders, R.string.async_shaders_description, false, asyncShaders));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE, Settings.SECTION_LAYOUT, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, 30, 100, "%", 85, cardboardScreenSize));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, -100, 100, "%", 0, cardboardXShift));
|
||||
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, -100, 100, "%", 0, cardboardYShift));
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.utility, 0));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING, Settings.SECTION_UTILITY, R.string.async_custom_loading, R.string.async_custom_loading_description, true, asyncCustomLoading));
|
||||
//Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra.
|
||||
//sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures));
|
||||
}
|
||||
|
||||
private void addAudioSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_audio);
|
||||
|
||||
SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO);
|
||||
Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING);
|
||||
Setting audioInputType = audioSection.getSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE);
|
||||
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 0, audioInputType));
|
||||
}
|
||||
|
||||
private void addDebugSettings(ArrayList<SettingsItem> sl) {
|
||||
mView.getActivity().setTitle(R.string.preferences_debug);
|
||||
|
||||
SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE);
|
||||
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
|
||||
Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT);
|
||||
Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER);
|
||||
Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC);
|
||||
Setting rendererDebug = rendererSection.getSetting(SettingsFile.KEY_RENDERER_DEBUG);
|
||||
|
||||
sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable));
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_RENDERER_DEBUG, Settings.SECTION_DEBUG, R.string.renderer_debug, R.string.renderer_debug_description, false, rendererDebug));
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Abstraction for a screen showing a list of settings. Instances of
|
||||
* this type of view will each display a layer of the setting hierarchy.
|
||||
*/
|
||||
public interface SettingsFragmentView {
|
||||
/**
|
||||
* Called by the containing Activity to notify the Fragment that an
|
||||
* asynchronous load operation completed.
|
||||
*
|
||||
* @param settings The (possibly null) result of the ini load operation.
|
||||
*/
|
||||
void onSettingsFileLoaded(Settings settings);
|
||||
|
||||
/**
|
||||
* Pass a settings HashMap to the containing activity, so that it can
|
||||
* share the HashMap with other SettingsFragments; useful so that rotations
|
||||
* do not require an additional load operation.
|
||||
*
|
||||
* @param settings An ArrayList containing all the settings HashMaps.
|
||||
*/
|
||||
void passSettingsToActivity(Settings settings);
|
||||
|
||||
/**
|
||||
* Pass an ArrayList to the View so that it can be displayed on screen.
|
||||
*
|
||||
* @param settingsList The result of converting the HashMap to an ArrayList
|
||||
*/
|
||||
void showSettingsList(ArrayList<SettingsItem> settingsList);
|
||||
|
||||
/**
|
||||
* Called by the containing Activity when an asynchronous load operation fails.
|
||||
* Instructs the Fragment to load the settings screen with defaults selected.
|
||||
*/
|
||||
void loadDefaultSettings();
|
||||
|
||||
/**
|
||||
* @return The Fragment's containing activity.
|
||||
*/
|
||||
FragmentActivity getActivity();
|
||||
|
||||
/**
|
||||
* Tell the Fragment to tell the containing Activity to show a new
|
||||
* Fragment containing a submenu of settings.
|
||||
*
|
||||
* @param menuKey Identifier for the settings group that should be shown.
|
||||
*/
|
||||
void loadSubMenu(String menuKey);
|
||||
|
||||
/**
|
||||
* Tell the Fragment to tell the containing activity to display a toast message.
|
||||
*
|
||||
* @param message Text to be shown in the Toast
|
||||
* @param is_long Whether this should be a long Toast or short one.
|
||||
*/
|
||||
void showToastMessage(String message, boolean is_long);
|
||||
|
||||
/**
|
||||
* Have the fragment add a setting to the HashMap.
|
||||
*
|
||||
* @param setting The (possibly previously missing) new setting.
|
||||
*/
|
||||
void putSetting(Setting setting);
|
||||
|
||||
/**
|
||||
* Have the fragment tell the containing Activity that a setting was modified.
|
||||
*/
|
||||
void onSettingChanged();
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class CheckBoxSettingViewHolder extends SettingViewHolder {
|
||||
private CheckBoxSetting mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
private CheckBox mCheckbox;
|
||||
|
||||
public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
mCheckbox = root.findViewById(R.id.checkbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mItem = (CheckBoxSetting) item;
|
||||
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
|
||||
if (item.getDescriptionId() > 0) {
|
||||
mTextSettingDescription.setText(item.getDescriptionId());
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTextSettingDescription.setText("");
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
mCheckbox.setChecked(mItem.isChecked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
mCheckbox.toggle();
|
||||
|
||||
getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked());
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
public final class DateTimeViewHolder extends SettingViewHolder {
|
||||
private DateTimeSetting mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
public DateTimeViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
Log.error("test " + mTextSettingName);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
Log.error("test " + mTextSettingDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mItem = (DateTimeSetting) item;
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
|
||||
if (item.getDescriptionId() > 0) {
|
||||
mTextSettingDescription.setText(item.getDescriptionId());
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
getAdapter().onDateTimeClick(mItem, getAdapterPosition());
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class HeaderViewHolder extends SettingViewHolder {
|
||||
private TextView mHeaderName;
|
||||
|
||||
public HeaderViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mHeaderName = root.findViewById(R.id.text_header_name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mHeaderName.setText(item.getNameId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
// no-op
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class InputBindingSettingViewHolder extends SettingViewHolder {
|
||||
private InputBindingSetting mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
private Context mContext;
|
||||
|
||||
public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) {
|
||||
super(itemView, adapter);
|
||||
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
|
||||
|
||||
mItem = (InputBindingSetting) item;
|
||||
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
|
||||
String key = sharedPreferences.getString(mItem.getKey(), "");
|
||||
if (key != null && !key.isEmpty()) {
|
||||
mTextSettingDescription.setText(key);
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
getAdapter().onInputBindingClick(mItem, getAdapterPosition());
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
|
||||
public final class PremiumViewHolder extends SettingViewHolder {
|
||||
private TextView mHeaderName;
|
||||
private TextView mTextDescription;
|
||||
private SettingsFragmentView mView;
|
||||
|
||||
public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) {
|
||||
super(itemView, adapter);
|
||||
mView = view;
|
||||
itemView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mHeaderName = root.findViewById(R.id.text_setting_name);
|
||||
mTextDescription = root.findViewById(R.id.text_setting_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
updateText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
if (MainActivity.isPremiumActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke billing flow if Premium is not already active, then refresh the UI to indicate
|
||||
// the purchase has completed.
|
||||
MainActivity.invokePremiumBilling(() -> updateText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the text shown to the user, based on whether Premium is active
|
||||
*/
|
||||
private void updateText() {
|
||||
if (MainActivity.isPremiumActive()) {
|
||||
mHeaderName.setText(R.string.premium_settings_welcome);
|
||||
mTextDescription.setText(R.string.premium_settings_welcome_description);
|
||||
} else {
|
||||
mHeaderName.setText(R.string.premium_settings_upsell);
|
||||
mTextDescription.setText(R.string.premium_settings_upsell_description);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
private SettingsAdapter mAdapter;
|
||||
|
||||
public SettingViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView);
|
||||
|
||||
mAdapter = adapter;
|
||||
|
||||
itemView.setOnClickListener(this);
|
||||
|
||||
findViews(itemView);
|
||||
}
|
||||
|
||||
protected SettingsAdapter getAdapter() {
|
||||
return mAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets handles to all this ViewHolder's child views using their XML-defined identifiers.
|
||||
*
|
||||
* @param root The newly inflated top-level view.
|
||||
*/
|
||||
protected abstract void findViews(View root);
|
||||
|
||||
/**
|
||||
* Called by the adapter to set this ViewHolder's child views to display the list item
|
||||
* it must now represent.
|
||||
*
|
||||
* @param item The list item that should be represented by this ViewHolder.
|
||||
*/
|
||||
public abstract void bind(SettingsItem item);
|
||||
|
||||
/**
|
||||
* Called when this ViewHolder's view is clicked on. Implementations should usually pass
|
||||
* this event up to the adapter.
|
||||
*
|
||||
* @param clicked The view that was clicked on.
|
||||
*/
|
||||
public abstract void onClick(View clicked);
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class SingleChoiceViewHolder extends SettingViewHolder {
|
||||
private SettingsItem mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mItem = item;
|
||||
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
if (item.getDescriptionId() > 0) {
|
||||
mTextSettingDescription.setText(item.getDescriptionId());
|
||||
} else if (item instanceof SingleChoiceSetting) {
|
||||
SingleChoiceSetting setting = (SingleChoiceSetting) item;
|
||||
int selected = setting.getSelectedValue();
|
||||
Resources resMgr = mTextSettingDescription.getContext().getResources();
|
||||
String[] choices = resMgr.getStringArray(setting.getChoicesId());
|
||||
int[] values = resMgr.getIntArray(setting.getValuesId());
|
||||
for (int i = 0; i < values.length; ++i) {
|
||||
if (values[i] == selected) {
|
||||
mTextSettingDescription.setText(choices[i]);
|
||||
}
|
||||
}
|
||||
} else if (item instanceof PremiumSingleChoiceSetting) {
|
||||
PremiumSingleChoiceSetting setting = (PremiumSingleChoiceSetting) item;
|
||||
int selected = setting.getSelectedValue();
|
||||
Resources resMgr = mTextSettingDescription.getContext().getResources();
|
||||
String[] choices = resMgr.getStringArray(setting.getChoicesId());
|
||||
int[] values = resMgr.getIntArray(setting.getValuesId());
|
||||
for (int i = 0; i < values.length; ++i) {
|
||||
if (values[i] == selected) {
|
||||
mTextSettingDescription.setText(choices[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
int position = getAdapterPosition();
|
||||
if (mItem instanceof SingleChoiceSetting) {
|
||||
getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position);
|
||||
} else if (mItem instanceof PremiumSingleChoiceSetting) {
|
||||
getAdapter().onSingleChoiceClick((PremiumSingleChoiceSetting) mItem, position);
|
||||
} else if (mItem instanceof StringSingleChoiceSetting) {
|
||||
getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class SliderViewHolder extends SettingViewHolder {
|
||||
private SliderSetting mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
public SliderViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mItem = (SliderSetting) item;
|
||||
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
|
||||
if (item.getDescriptionId() > 0) {
|
||||
mTextSettingDescription.setText(item.getDescriptionId());
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
getAdapter().onSliderClick(mItem, getAdapterPosition());
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
public final class SubmenuViewHolder extends SettingViewHolder {
|
||||
private SubmenuSetting mItem;
|
||||
|
||||
private TextView mTextSettingName;
|
||||
private TextView mTextSettingDescription;
|
||||
|
||||
public SubmenuViewHolder(View itemView, SettingsAdapter adapter) {
|
||||
super(itemView, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mTextSettingName = root.findViewById(R.id.text_setting_name);
|
||||
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
mItem = (SubmenuSetting) item;
|
||||
|
||||
mTextSettingName.setText(item.getNameId());
|
||||
|
||||
if (item.getDescriptionId() > 0) {
|
||||
mTextSettingDescription.setText(item.getDescriptionId());
|
||||
mTextSettingDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mTextSettingDescription.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
getAdapter().onSubmenuClick(mItem);
|
||||
}
|
||||
}
|
@ -1,362 +0,0 @@
|
||||
package org.citra.citra_emu.features.settings.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.FloatSetting;
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting;
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.SettingSection;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.model.StringSetting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
|
||||
import org.citra.citra_emu.utils.BiMap;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.ini4j.Wini;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Contains static methods for interacting with .ini files in which settings are stored.
|
||||
*/
|
||||
public final class SettingsFile {
|
||||
public static final String FILE_NAME_CONFIG = "config";
|
||||
|
||||
public static final String KEY_CPU_JIT = "use_cpu_jit";
|
||||
|
||||
public static final String KEY_DESIGN = "design";
|
||||
|
||||
public static final String KEY_PREMIUM = "premium";
|
||||
|
||||
public static final String KEY_GRAPHICS_API = "graphics_api";
|
||||
public static final String KEY_SPIRV_SHADER_GEN = "spirv_shader_gen";
|
||||
public static final String KEY_ASYNC_SHADERS = "async_shader_compilation";
|
||||
public static final String KEY_RENDERER_DEBUG = "renderer_debug";
|
||||
public static final String KEY_HW_SHADER = "use_hw_shader";
|
||||
public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul";
|
||||
public static final String KEY_USE_SHADER_JIT = "use_shader_jit";
|
||||
public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache";
|
||||
public static final String KEY_USE_VSYNC = "use_vsync_new";
|
||||
public static final String KEY_RESOLUTION_FACTOR = "resolution_factor";
|
||||
public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit";
|
||||
public static final String KEY_FRAME_LIMIT = "frame_limit";
|
||||
public static final String KEY_BACKGROUND_RED = "bg_red";
|
||||
public static final String KEY_BACKGROUND_BLUE = "bg_blue";
|
||||
public static final String KEY_BACKGROUND_GREEN = "bg_green";
|
||||
public static final String KEY_RENDER_3D = "render_3d";
|
||||
public static final String KEY_FACTOR_3D = "factor_3d";
|
||||
public static final String KEY_PP_SHADER_NAME = "pp_shader_name";
|
||||
public static final String KEY_FILTER_MODE = "filter_mode";
|
||||
public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name";
|
||||
public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation";
|
||||
|
||||
public static final String KEY_LAYOUT_OPTION = "layout_option";
|
||||
public static final String KEY_SWAP_SCREEN = "swap_screen";
|
||||
public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size";
|
||||
public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift";
|
||||
public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift";
|
||||
|
||||
public static final String KEY_DUMP_TEXTURES = "dump_textures";
|
||||
public static final String KEY_CUSTOM_TEXTURES = "custom_textures";
|
||||
public static final String KEY_PRELOAD_TEXTURES = "preload_textures";
|
||||
public static final String KEY_ASYNC_CUSTOM_LOADING = "async_custom_loading";
|
||||
|
||||
public static final String KEY_AUDIO_OUTPUT_TYPE = "output_type";
|
||||
public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
|
||||
public static final String KEY_VOLUME = "volume";
|
||||
public static final String KEY_AUDIO_INPUT_TYPE = "input_type";
|
||||
|
||||
public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd";
|
||||
|
||||
public static final String KEY_IS_NEW_3DS = "is_new_3ds";
|
||||
public static final String KEY_REGION_VALUE = "region_value";
|
||||
public static final String KEY_LANGUAGE = "language";
|
||||
public static final String KEY_PLUGIN_LOADER = "plugin_loader";
|
||||
public static final String KEY_ALLOW_PLUGIN_LOADER = "allow_plugin_loader";
|
||||
|
||||
public static final String KEY_INIT_CLOCK = "init_clock";
|
||||
public static final String KEY_INIT_TIME = "init_time";
|
||||
|
||||
public static final String KEY_BUTTON_A = "button_a";
|
||||
public static final String KEY_BUTTON_B = "button_b";
|
||||
public static final String KEY_BUTTON_X = "button_x";
|
||||
public static final String KEY_BUTTON_Y = "button_y";
|
||||
public static final String KEY_BUTTON_SELECT = "button_select";
|
||||
public static final String KEY_BUTTON_START = "button_start";
|
||||
public static final String KEY_BUTTON_UP = "button_up";
|
||||
public static final String KEY_BUTTON_DOWN = "button_down";
|
||||
public static final String KEY_BUTTON_LEFT = "button_left";
|
||||
public static final String KEY_BUTTON_RIGHT = "button_right";
|
||||
public static final String KEY_BUTTON_L = "button_l";
|
||||
public static final String KEY_BUTTON_R = "button_r";
|
||||
public static final String KEY_BUTTON_ZL = "button_zl";
|
||||
public static final String KEY_BUTTON_ZR = "button_zr";
|
||||
public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical";
|
||||
public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal";
|
||||
public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical";
|
||||
public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal";
|
||||
public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical";
|
||||
public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal";
|
||||
public static final String KEY_CIRCLEPAD_UP = "circlepad_up";
|
||||
public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down";
|
||||
public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left";
|
||||
public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right";
|
||||
public static final String KEY_CSTICK_UP = "cstick_up";
|
||||
public static final String KEY_CSTICK_DOWN = "cstick_down";
|
||||
public static final String KEY_CSTICK_LEFT = "cstick_left";
|
||||
public static final String KEY_CSTICK_RIGHT = "cstick_right";
|
||||
|
||||
public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name";
|
||||
public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config";
|
||||
public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip";
|
||||
public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name";
|
||||
public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config";
|
||||
public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip";
|
||||
public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name";
|
||||
public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config";
|
||||
public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip";
|
||||
|
||||
public static final String KEY_LOG_FILTER = "log_filter";
|
||||
|
||||
private static BiMap<String, String> sectionsMap = new BiMap<>();
|
||||
|
||||
static {
|
||||
//TODO: Add members to sectionsMap when game-specific settings are added
|
||||
}
|
||||
|
||||
|
||||
private SettingsFile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
|
||||
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
|
||||
* failed.
|
||||
*
|
||||
* @param ini The ini file to load the settings from
|
||||
* @param isCustomGame
|
||||
* @param view The current view.
|
||||
* @return An Observable that emits a HashMap of the file's contents, then completes.
|
||||
*/
|
||||
static HashMap<String, SettingSection> readFile(final DocumentFile ini, boolean isCustomGame, SettingsActivityView view) {
|
||||
HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
|
||||
|
||||
BufferedReader reader = null;
|
||||
|
||||
try {
|
||||
Context context = CitraApplication.getAppContext();
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
|
||||
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
|
||||
SettingSection current = null;
|
||||
for (String line; (line = reader.readLine()) != null; ) {
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
current = sectionFromLine(line, isCustomGame);
|
||||
sections.put(current.getName(), current);
|
||||
} else if ((current != null)) {
|
||||
Setting setting = settingFromLine(current, line);
|
||||
if (setting != null) {
|
||||
current.putSetting(setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.error("[SettingsFile] File not found: " + ini.getUri() + e.getMessage());
|
||||
if (view != null)
|
||||
view.onSettingsFileNotFound();
|
||||
} catch (IOException e) {
|
||||
Log.error("[SettingsFile] Error reading from: " + ini.getUri() + e.getMessage());
|
||||
if (view != null)
|
||||
view.onSettingsFileNotFound();
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
Log.error("[SettingsFile] Error closing: " + ini.getUri() + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
public static HashMap<String, SettingSection> readFile(final String fileName, SettingsActivityView view) {
|
||||
return readFile(getSettingsFile(fileName), false, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
|
||||
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
|
||||
* failed.
|
||||
*
|
||||
* @param gameId the id of the game to load it's settings.
|
||||
* @param view The current view.
|
||||
*/
|
||||
public static HashMap<String, SettingSection> readCustomGameSettings(final String gameId, SettingsActivityView view) {
|
||||
return readFile(getCustomGameSettingsFile(gameId), true, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
|
||||
* telling why it failed.
|
||||
*
|
||||
* @param fileName The target filename without a path or extension.
|
||||
* @param sections The HashMap containing the Settings we want to serialize.
|
||||
* @param view The current view.
|
||||
*/
|
||||
public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections,
|
||||
SettingsActivityView view) {
|
||||
DocumentFile ini = getSettingsFile(fileName);
|
||||
|
||||
try {
|
||||
Context context = CitraApplication.getAppContext();
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
|
||||
Wini writer = new Wini(inputStream);
|
||||
|
||||
Set<String> keySet = sections.keySet();
|
||||
for (String key : keySet) {
|
||||
SettingSection section = sections.get(key);
|
||||
writeSection(writer, section);
|
||||
}
|
||||
inputStream.close();
|
||||
OutputStream outputStream = context.getContentResolver().openOutputStream(ini.getUri(), "wt");
|
||||
writer.store(outputStream);
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
|
||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void saveCustomGameSettings(final String gameId, final HashMap<String, SettingSection> sections) {
|
||||
Set<String> sortedSections = new TreeSet<>(sections.keySet());
|
||||
|
||||
for (String sectionKey : sortedSections) {
|
||||
SettingSection section = sections.get(sectionKey);
|
||||
|
||||
HashMap<String, Setting> settings = section.getSettings();
|
||||
Set<String> sortedKeySet = new TreeSet<>(settings.keySet());
|
||||
|
||||
for (String settingKey : sortedKeySet) {
|
||||
Setting setting = settings.get(settingKey);
|
||||
NativeLibrary.SetUserSetting(gameId, mapSectionNameFromIni(section.getName()), setting.getKey(), setting.getValueAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String mapSectionNameFromIni(String generalSectionName) {
|
||||
if (sectionsMap.getForward(generalSectionName) != null) {
|
||||
return sectionsMap.getForward(generalSectionName);
|
||||
}
|
||||
|
||||
return generalSectionName;
|
||||
}
|
||||
|
||||
private static String mapSectionNameToIni(String generalSectionName) {
|
||||
if (sectionsMap.getBackward(generalSectionName) != null) {
|
||||
return sectionsMap.getBackward(generalSectionName);
|
||||
}
|
||||
|
||||
return generalSectionName;
|
||||
}
|
||||
|
||||
public static DocumentFile getSettingsFile(String fileName) {
|
||||
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.getAppContext(), Uri.parse(DirectoryInitialization.getUserDirectory()));
|
||||
DocumentFile configDirectory = root.findFile("config");
|
||||
return configDirectory.findFile(fileName + ".ini");
|
||||
}
|
||||
|
||||
private static DocumentFile getCustomGameSettingsFile(String gameId) {
|
||||
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.getAppContext(), Uri.parse(DirectoryInitialization.getUserDirectory()));
|
||||
DocumentFile configDirectory = root.findFile("GameSettings");
|
||||
return configDirectory.findFile(gameId + ".ini");
|
||||
}
|
||||
|
||||
private static SettingSection sectionFromLine(String line, boolean isCustomGame) {
|
||||
String sectionName = line.substring(1, line.length() - 1);
|
||||
if (isCustomGame) {
|
||||
sectionName = mapSectionNameToIni(sectionName);
|
||||
}
|
||||
return new SettingSection(sectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a line of text, determines what type of data is being represented, and returns
|
||||
* a Setting object containing this data.
|
||||
*
|
||||
* @param current The section currently being parsed by the consuming method.
|
||||
* @param line The line of text being parsed.
|
||||
* @return A typed Setting containing the key/value contained in the line.
|
||||
*/
|
||||
private static Setting settingFromLine(SettingSection current, String line) {
|
||||
String[] splitLine = line.split("=");
|
||||
|
||||
if (splitLine.length != 2) {
|
||||
Log.warning("Skipping invalid config line \"" + line + "\"");
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = splitLine[0].trim();
|
||||
String value = splitLine[1].trim();
|
||||
|
||||
if (value.isEmpty()) {
|
||||
Log.warning("Skipping null value in config line \"" + line + "\"");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
int valueAsInt = Integer.parseInt(value);
|
||||
|
||||
return new IntSetting(key, current.getName(), valueAsInt);
|
||||
} catch (NumberFormatException ex) {
|
||||
}
|
||||
|
||||
try {
|
||||
float valueAsFloat = Float.parseFloat(value);
|
||||
|
||||
return new FloatSetting(key, current.getName(), valueAsFloat);
|
||||
} catch (NumberFormatException ex) {
|
||||
}
|
||||
|
||||
return new StringSetting(key, current.getName(), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contents of a Section HashMap to disk.
|
||||
*
|
||||
* @param parser A Wini pointed at a file on disk.
|
||||
* @param section A section containing settings to be written to the file.
|
||||
*/
|
||||
private static void writeSection(Wini parser, SettingSection section) {
|
||||
// Write the section header.
|
||||
String header = section.getName();
|
||||
|
||||
// Write this section's values.
|
||||
HashMap<String, Setting> settings = section.getSettings();
|
||||
Set<String> keySet = settings.keySet();
|
||||
|
||||
for (String key : keySet) {
|
||||
Setting setting = settings.get(key);
|
||||
parser.put(header, setting.getKey(), setting.getValueAsString());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,378 +0,0 @@
|
||||
package org.citra.citra_emu.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.Choreographer;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.overlay.InputOverlay;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback {
|
||||
private static final String KEY_GAMEPATH = "gamepath";
|
||||
|
||||
private static final Handler perfStatsUpdateHandler = new Handler();
|
||||
|
||||
private SharedPreferences mPreferences;
|
||||
|
||||
private InputOverlay mInputOverlay;
|
||||
|
||||
private EmulationState mEmulationState;
|
||||
|
||||
private DirectoryStateReceiver directoryStateReceiver;
|
||||
|
||||
private EmulationActivity activity;
|
||||
|
||||
private TextView mPerfStats;
|
||||
|
||||
private Runnable perfStatsUpdater;
|
||||
|
||||
public static EmulationFragment newInstance(String gamePath) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(KEY_GAMEPATH, gamePath);
|
||||
|
||||
EmulationFragment fragment = new EmulationFragment();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (context instanceof EmulationActivity) {
|
||||
activity = (EmulationActivity) context;
|
||||
NativeLibrary.setEmulationActivity((EmulationActivity) context);
|
||||
} else {
|
||||
throw new IllegalStateException("EmulationFragment must have EmulationActivity parent");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize anything that doesn't depend on the layout / views in here.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// So this fragment doesn't restart on configuration changes; i.e. rotation.
|
||||
setRetainInstance(true);
|
||||
|
||||
mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
|
||||
String gamePath = getArguments().getString(KEY_GAMEPATH);
|
||||
mEmulationState = new EmulationState(gamePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the UI and start emulation in here.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View contents = inflater.inflate(R.layout.fragment_emulation, container, false);
|
||||
|
||||
SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation);
|
||||
surfaceView.getHolder().addCallback(this);
|
||||
|
||||
mInputOverlay = contents.findViewById(R.id.surface_input_overlay);
|
||||
mPerfStats = contents.findViewById(R.id.show_fps_text);
|
||||
|
||||
Button doneButton = contents.findViewById(R.id.done_control_config);
|
||||
if (doneButton != null) {
|
||||
doneButton.setOnClickListener(v -> stopConfiguringControls());
|
||||
}
|
||||
|
||||
// Show/hide the "Show FPS" overlay
|
||||
updateShowFpsOverlay();
|
||||
|
||||
// The new Surface created here will get passed to the native code via onSurfaceChanged.
|
||||
return contents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Choreographer.getInstance().postFrameCallback(this);
|
||||
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||
mEmulationState.run(activity.isActivityRecreated());
|
||||
} else {
|
||||
setupCitraDirectoriesThenStartEmulation();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (directoryStateReceiver != null) {
|
||||
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver);
|
||||
directoryStateReceiver = null;
|
||||
}
|
||||
|
||||
if (mEmulationState.isRunning()) {
|
||||
mEmulationState.pause();
|
||||
}
|
||||
|
||||
Choreographer.getInstance().removeFrameCallback(this);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
NativeLibrary.clearEmulationActivity();
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
private void setupCitraDirectoriesThenStartEmulation() {
|
||||
IntentFilter statusIntentFilter = new IntentFilter(
|
||||
DirectoryInitialization.BROADCAST_ACTION);
|
||||
|
||||
directoryStateReceiver =
|
||||
new DirectoryStateReceiver(directoryInitializationState ->
|
||||
{
|
||||
if (directoryInitializationState ==
|
||||
DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
|
||||
mEmulationState.run(activity.isActivityRecreated());
|
||||
} else if (directoryInitializationState ==
|
||||
DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
|
||||
Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
} else if (directoryInitializationState ==
|
||||
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
||||
Toast.makeText(getContext(), R.string.external_storage_not_mounted,
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Registers the DirectoryStateReceiver and its intent filters
|
||||
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
|
||||
directoryStateReceiver,
|
||||
statusIntentFilter);
|
||||
DirectoryInitialization.start(getActivity());
|
||||
}
|
||||
|
||||
public void refreshInputOverlay() {
|
||||
mInputOverlay.refreshControls();
|
||||
}
|
||||
|
||||
public void resetInputOverlay() {
|
||||
// Reset button scale
|
||||
SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putInt("controlScale", 50);
|
||||
editor.apply();
|
||||
|
||||
mInputOverlay.resetButtonPlacement();
|
||||
}
|
||||
|
||||
public void updateShowFpsOverlay() {
|
||||
if (EmulationMenuSettings.getShowFps()) {
|
||||
final int SYSTEM_FPS = 0;
|
||||
final int FPS = 1;
|
||||
final int FRAMETIME = 2;
|
||||
final int SPEED = 3;
|
||||
|
||||
perfStatsUpdater = () ->
|
||||
{
|
||||
final double[] perfStats = NativeLibrary.GetPerfStats();
|
||||
if (perfStats[FPS] > 0) {
|
||||
mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS] + 0.5),
|
||||
(int) (perfStats[SPEED] * 100.0 + 0.5)));
|
||||
}
|
||||
|
||||
perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000);
|
||||
};
|
||||
perfStatsUpdateHandler.post(perfStatsUpdater);
|
||||
|
||||
mPerfStats.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
if (perfStatsUpdater != null) {
|
||||
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater);
|
||||
}
|
||||
|
||||
mPerfStats.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
// We purposely don't do anything here.
|
||||
// All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height);
|
||||
mEmulationState.newSurface(holder.getSurface());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
mEmulationState.clearSurface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
Choreographer.getInstance().postFrameCallback(this);
|
||||
NativeLibrary.DoFrame();
|
||||
}
|
||||
|
||||
public void stopEmulation() {
|
||||
mEmulationState.stop();
|
||||
}
|
||||
|
||||
public void startConfiguringControls() {
|
||||
getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE);
|
||||
mInputOverlay.setIsInEditMode(true);
|
||||
}
|
||||
|
||||
public void stopConfiguringControls() {
|
||||
getView().findViewById(R.id.done_control_config).setVisibility(View.GONE);
|
||||
mInputOverlay.setIsInEditMode(false);
|
||||
}
|
||||
|
||||
public boolean isConfiguringControls() {
|
||||
return mInputOverlay.isInEditMode();
|
||||
}
|
||||
|
||||
private static class EmulationState {
|
||||
private final String mGamePath;
|
||||
private State state;
|
||||
private Surface mSurface;
|
||||
private boolean mRunWhenSurfaceIsValid;
|
||||
|
||||
EmulationState(String gamePath) {
|
||||
mGamePath = gamePath;
|
||||
// Starting state is stopped.
|
||||
state = State.STOPPED;
|
||||
}
|
||||
|
||||
public synchronized boolean isStopped() {
|
||||
return state == State.STOPPED;
|
||||
}
|
||||
|
||||
// Getters for the current state
|
||||
|
||||
public synchronized boolean isPaused() {
|
||||
return state == State.PAUSED;
|
||||
}
|
||||
|
||||
public synchronized boolean isRunning() {
|
||||
return state == State.RUNNING;
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
if (state != State.STOPPED) {
|
||||
Log.debug("[EmulationFragment] Stopping emulation.");
|
||||
state = State.STOPPED;
|
||||
NativeLibrary.StopEmulation();
|
||||
} else {
|
||||
Log.warning("[EmulationFragment] Stop called while already stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
// State changing methods
|
||||
|
||||
public synchronized void pause() {
|
||||
if (state != State.PAUSED) {
|
||||
state = State.PAUSED;
|
||||
Log.debug("[EmulationFragment] Pausing emulation.");
|
||||
|
||||
// Release the surface before pausing, since emulation has to be running for that.
|
||||
NativeLibrary.SurfaceDestroyed();
|
||||
NativeLibrary.PauseEmulation();
|
||||
} else {
|
||||
Log.warning("[EmulationFragment] Pause called while already paused.");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void run(boolean isActivityRecreated) {
|
||||
if (isActivityRecreated) {
|
||||
if (NativeLibrary.IsRunning()) {
|
||||
state = State.PAUSED;
|
||||
}
|
||||
} else {
|
||||
Log.debug("[EmulationFragment] activity resumed or fresh start");
|
||||
}
|
||||
|
||||
// If the surface is set, run now. Otherwise, wait for it to get set.
|
||||
if (mSurface != null) {
|
||||
runWithValidSurface();
|
||||
} else {
|
||||
mRunWhenSurfaceIsValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Surface callbacks
|
||||
public synchronized void newSurface(Surface surface) {
|
||||
mSurface = surface;
|
||||
if (mRunWhenSurfaceIsValid) {
|
||||
runWithValidSurface();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clearSurface() {
|
||||
if (mSurface == null) {
|
||||
Log.warning("[EmulationFragment] clearSurface called, but surface already null.");
|
||||
} else {
|
||||
mSurface = null;
|
||||
Log.debug("[EmulationFragment] Surface destroyed.");
|
||||
|
||||
if (state == State.RUNNING) {
|
||||
NativeLibrary.SurfaceDestroyed();
|
||||
state = State.PAUSED;
|
||||
} else if (state == State.PAUSED) {
|
||||
Log.warning("[EmulationFragment] Surface cleared while emulation paused.");
|
||||
} else {
|
||||
Log.warning("[EmulationFragment] Surface cleared while emulation stopped.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void runWithValidSurface() {
|
||||
mRunWhenSurfaceIsValid = false;
|
||||
if (state == State.STOPPED) {
|
||||
NativeLibrary.SurfaceChanged(mSurface);
|
||||
Thread mEmulationThread = new Thread(() ->
|
||||
{
|
||||
Log.debug("[EmulationFragment] Starting emulation thread.");
|
||||
NativeLibrary.Run(mGamePath);
|
||||
}, "NativeEmulation");
|
||||
mEmulationThread.start();
|
||||
|
||||
} else if (state == State.PAUSED) {
|
||||
Log.debug("[EmulationFragment] Resuming emulation.");
|
||||
NativeLibrary.SurfaceChanged(mSurface);
|
||||
NativeLibrary.UnPauseEmulation();
|
||||
} else {
|
||||
Log.debug("[EmulationFragment] Bug, run called while already running.");
|
||||
}
|
||||
state = State.RUNNING;
|
||||
}
|
||||
|
||||
private enum State {
|
||||
STOPPED, RUNNING, PAUSED
|
||||
}
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package org.citra.citra_emu.model;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public final class Game {
|
||||
private String mTitle;
|
||||
private String mDescription;
|
||||
private String mPath;
|
||||
private String mGameId;
|
||||
private String mCompany;
|
||||
private String mRegions;
|
||||
|
||||
public Game(String title, String description, String regions, String path,
|
||||
String gameId, String company) {
|
||||
mTitle = title;
|
||||
mDescription = description;
|
||||
mRegions = regions;
|
||||
mPath = path;
|
||||
mGameId = gameId;
|
||||
mCompany = company;
|
||||
}
|
||||
|
||||
public static ContentValues asContentValues(String title, String description, String regions, String path, String gameId, String company) {
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
if (gameId.isEmpty()) {
|
||||
// Homebrew, etc. may not have a game ID, use filename as a unique identifier
|
||||
gameId = Paths.get(path).getFileName().toString();
|
||||
}
|
||||
|
||||
values.put(GameDatabase.KEY_GAME_TITLE, title);
|
||||
values.put(GameDatabase.KEY_GAME_DESCRIPTION, description);
|
||||
values.put(GameDatabase.KEY_GAME_REGIONS, regions);
|
||||
values.put(GameDatabase.KEY_GAME_PATH, path);
|
||||
values.put(GameDatabase.KEY_GAME_ID, gameId);
|
||||
values.put(GameDatabase.KEY_GAME_COMPANY, company);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public static Game fromCursor(Cursor cursor) {
|
||||
return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE),
|
||||
cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
|
||||
cursor.getString(GameDatabase.GAME_COLUMN_REGIONS),
|
||||
cursor.getString(GameDatabase.GAME_COLUMN_PATH),
|
||||
cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
|
||||
cursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
public String getCompany() {
|
||||
return mCompany;
|
||||
}
|
||||
|
||||
public String getRegions() {
|
||||
return mRegions;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public String getGameId() {
|
||||
return mGameId;
|
||||
}
|
||||
}
|
@ -1,279 +0,0 @@
|
||||
package org.citra.citra_emu.model;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import rx.Observable;
|
||||
|
||||
/**
|
||||
* A helper class that provides several utilities simplifying interaction with
|
||||
* the SQLite database.
|
||||
*/
|
||||
public final class GameDatabase extends SQLiteOpenHelper {
|
||||
public static final int COLUMN_DB_ID = 0;
|
||||
public static final int GAME_COLUMN_PATH = 1;
|
||||
public static final int GAME_COLUMN_TITLE = 2;
|
||||
public static final int GAME_COLUMN_DESCRIPTION = 3;
|
||||
public static final int GAME_COLUMN_REGIONS = 4;
|
||||
public static final int GAME_COLUMN_GAME_ID = 5;
|
||||
public static final int GAME_COLUMN_COMPANY = 6;
|
||||
public static final int FOLDER_COLUMN_PATH = 1;
|
||||
public static final String KEY_DB_ID = "_id";
|
||||
public static final String KEY_GAME_PATH = "path";
|
||||
public static final String KEY_GAME_TITLE = "title";
|
||||
public static final String KEY_GAME_DESCRIPTION = "description";
|
||||
public static final String KEY_GAME_REGIONS = "regions";
|
||||
public static final String KEY_GAME_ID = "game_id";
|
||||
public static final String KEY_GAME_COMPANY = "company";
|
||||
public static final String KEY_FOLDER_PATH = "path";
|
||||
public static final String TABLE_NAME_FOLDERS = "folders";
|
||||
public static final String TABLE_NAME_GAMES = "games";
|
||||
private static final int DB_VERSION = 2;
|
||||
private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY";
|
||||
private static final String TYPE_INTEGER = " INTEGER";
|
||||
private static final String TYPE_STRING = " TEXT";
|
||||
|
||||
private static final String CONSTRAINT_UNIQUE = " UNIQUE";
|
||||
|
||||
private static final String SEPARATOR = ", ";
|
||||
|
||||
private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "("
|
||||
+ KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
|
||||
+ KEY_GAME_PATH + TYPE_STRING + SEPARATOR
|
||||
+ KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
|
||||
+ KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
|
||||
+ KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR
|
||||
+ KEY_GAME_ID + TYPE_STRING + SEPARATOR
|
||||
+ KEY_GAME_COMPANY + TYPE_STRING + ")";
|
||||
|
||||
private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "("
|
||||
+ KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
|
||||
+ KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")";
|
||||
|
||||
private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
|
||||
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
|
||||
private final Context mContext;
|
||||
|
||||
public GameDatabase(Context context) {
|
||||
// Superclass constructor builds a database or uses an existing one.
|
||||
super(context, "games.db", null, DB_VERSION);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase database) {
|
||||
Log.debug("[GameDatabase] GameDatabase - Creating database...");
|
||||
|
||||
execSqlAndLog(database, SQL_CREATE_GAMES);
|
||||
execSqlAndLog(database, SQL_CREATE_FOLDERS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) {
|
||||
Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..");
|
||||
execSqlAndLog(database, SQL_DELETE_FOLDERS);
|
||||
execSqlAndLog(database, SQL_CREATE_FOLDERS);
|
||||
|
||||
execSqlAndLog(database, SQL_DELETE_GAMES);
|
||||
execSqlAndLog(database, SQL_CREATE_GAMES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) {
|
||||
Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " +
|
||||
newVersion);
|
||||
|
||||
// Delete all the games
|
||||
execSqlAndLog(database, SQL_DELETE_GAMES);
|
||||
execSqlAndLog(database, SQL_CREATE_GAMES);
|
||||
}
|
||||
|
||||
public void resetDatabase(SQLiteDatabase database) {
|
||||
execSqlAndLog(database, SQL_DELETE_FOLDERS);
|
||||
execSqlAndLog(database, SQL_CREATE_FOLDERS);
|
||||
|
||||
execSqlAndLog(database, SQL_DELETE_GAMES);
|
||||
execSqlAndLog(database, SQL_CREATE_GAMES);
|
||||
}
|
||||
|
||||
public void scanLibrary(SQLiteDatabase database) {
|
||||
// Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
|
||||
Cursor fileCursor = database.query(TABLE_NAME_GAMES,
|
||||
null, // Get all columns.
|
||||
null, // Get all rows.
|
||||
null,
|
||||
null, // No grouping.
|
||||
null,
|
||||
null); // Order of games is irrelevant.
|
||||
|
||||
// Possibly overly defensive, but ensures that moveToNext() does not skip a row.
|
||||
fileCursor.moveToPosition(-1);
|
||||
|
||||
while (fileCursor.moveToNext()) {
|
||||
String gamePath = fileCursor.getString(GAME_COLUMN_PATH);
|
||||
|
||||
if (!FileUtil.Exists(mContext, gamePath)) {
|
||||
Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
|
||||
gamePath);
|
||||
database.delete(TABLE_NAME_GAMES,
|
||||
KEY_DB_ID + " = ?",
|
||||
new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
|
||||
}
|
||||
}
|
||||
|
||||
// Get a cursor listing all the folders the user has added to the library.
|
||||
Cursor folderCursor = database.query(TABLE_NAME_FOLDERS,
|
||||
null, // Get all columns.
|
||||
null, // Get all rows.
|
||||
null,
|
||||
null, // No grouping.
|
||||
null,
|
||||
null); // Order of folders is irrelevant.
|
||||
|
||||
Set<String> allowedExtensions = new HashSet<String>(Arrays.asList(
|
||||
".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app", ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz"));
|
||||
|
||||
// Possibly overly defensive, but ensures that moveToNext() does not skip a row.
|
||||
folderCursor.moveToPosition(-1);
|
||||
|
||||
// Iterate through all results of the DB query (i.e. all folders in the library.)
|
||||
while (folderCursor.moveToNext()) {
|
||||
String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
|
||||
|
||||
Uri folder = Uri.parse(folderPath);
|
||||
// If the folder is empty because it no longer exists, remove it from the library.
|
||||
CheapDocument[] files = FileUtil.listFiles(mContext, folder);
|
||||
if (files.length == 0) {
|
||||
Log.error(
|
||||
"[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
|
||||
database.delete(TABLE_NAME_FOLDERS,
|
||||
KEY_DB_ID + " = ?",
|
||||
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
|
||||
}
|
||||
|
||||
addGamesRecursive(database, files, allowedExtensions, 3);
|
||||
}
|
||||
|
||||
fileCursor.close();
|
||||
folderCursor.close();
|
||||
|
||||
Arrays.stream(NativeLibrary.GetInstalledGamePaths())
|
||||
.forEach(filePath -> attemptToAddGame(database, filePath));
|
||||
|
||||
database.close();
|
||||
}
|
||||
|
||||
private void addGamesRecursive(SQLiteDatabase database, CheapDocument[] files,
|
||||
Set<String> allowedExtensions, int depth) {
|
||||
if (depth <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (CheapDocument file : files) {
|
||||
if (file.isDirectory()) {
|
||||
Set<String> newExtensions = new HashSet<>(Arrays.asList(
|
||||
".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app"));
|
||||
CheapDocument[] children = FileUtil.listFiles(mContext, file.getUri());
|
||||
this.addGamesRecursive(database, children, newExtensions, depth - 1);
|
||||
} else {
|
||||
String filename = file.getUri().toString();
|
||||
|
||||
int extensionStart = filename.lastIndexOf('.');
|
||||
if (extensionStart > 0) {
|
||||
String fileExtension = filename.substring(extensionStart);
|
||||
|
||||
// Check that the file has an extension we care about before trying to read out of it.
|
||||
if (allowedExtensions.contains(fileExtension.toLowerCase())) {
|
||||
attemptToAddGame(database, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void attemptToAddGame(SQLiteDatabase database, String filePath) {
|
||||
GameInfo gameInfo;
|
||||
try {
|
||||
gameInfo = new GameInfo(filePath);
|
||||
} catch (IOException e) {
|
||||
gameInfo = null;
|
||||
}
|
||||
|
||||
String name = gameInfo != null ? gameInfo.getTitle() : "";
|
||||
|
||||
// If the game's title field is empty, use the filename.
|
||||
if (name.isEmpty()) {
|
||||
name = filePath.substring(filePath.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
ContentValues game = Game.asContentValues(name,
|
||||
filePath.replace("\n", " "),
|
||||
gameInfo != null ? gameInfo.getRegions() : "Invalid region",
|
||||
filePath,
|
||||
filePath,
|
||||
gameInfo != null ? gameInfo.getCompany() : "");
|
||||
|
||||
// Try to update an existing game first.
|
||||
int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update.
|
||||
game,
|
||||
// The values to fill the row with.
|
||||
KEY_GAME_ID + " = ?",
|
||||
// The WHERE clause used to find the right row.
|
||||
new String[]{game.getAsString(
|
||||
KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this,
|
||||
// which is provided as an array because there
|
||||
// could potentially be more than one argument.
|
||||
|
||||
// If update fails, insert a new game instead.
|
||||
if (rowsMatched == 0) {
|
||||
Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE));
|
||||
database.insert(TABLE_NAME_GAMES, null, game);
|
||||
} else {
|
||||
Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE));
|
||||
}
|
||||
}
|
||||
|
||||
public Observable<Cursor> getGames() {
|
||||
return Observable.create(subscriber ->
|
||||
{
|
||||
Log.info("[GameDatabase] Reading games list...");
|
||||
|
||||
SQLiteDatabase database = getReadableDatabase();
|
||||
Cursor resultCursor = database.query(
|
||||
TABLE_NAME_GAMES,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
KEY_GAME_TITLE + " ASC"
|
||||
);
|
||||
|
||||
// Pass the result cursor to the consumer.
|
||||
subscriber.onNext(resultCursor);
|
||||
|
||||
// Tell the consumer we're done; it will unsubscribe implicitly.
|
||||
subscriber.onCompleted();
|
||||
});
|
||||
}
|
||||
|
||||
private void execSqlAndLog(SQLiteDatabase database, String sql) {
|
||||
Log.verbose("[GameDatabase] Executing SQL: " + sql);
|
||||
database.execSQL(sql);
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package org.citra.citra_emu.model;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class GameInfo {
|
||||
@Keep
|
||||
private final long mPointer;
|
||||
|
||||
@Keep
|
||||
public GameInfo(String path) throws IOException {
|
||||
mPointer = initialize(path);
|
||||
if (mPointer == 0L) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static native long initialize(String path);
|
||||
|
||||
@Override
|
||||
protected native void finalize();
|
||||
|
||||
@NonNull
|
||||
public native String getTitle();
|
||||
|
||||
@NonNull
|
||||
public native String getRegions();
|
||||
|
||||
@NonNull
|
||||
public native String getCompany();
|
||||
|
||||
@Nullable
|
||||
public native int[] getIcon();
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
package org.citra.citra_emu.model;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.citra.citra_emu.BuildConfig;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
/**
|
||||
* Provides an interface allowing Activities to interact with the SQLite database.
|
||||
* CRUD methods in this class can be called by Activities using getContentResolver().
|
||||
*/
|
||||
public final class GameProvider extends ContentProvider {
|
||||
public static final String REFRESH_LIBRARY = "refresh";
|
||||
public static final String RESET_LIBRARY = "reset";
|
||||
|
||||
public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider";
|
||||
public static final Uri URI_FOLDER =
|
||||
Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/");
|
||||
public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/");
|
||||
public static final Uri URI_RESET = Uri.parse(AUTHORITY + "/" + RESET_LIBRARY + "/");
|
||||
|
||||
public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder";
|
||||
public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game";
|
||||
|
||||
|
||||
private GameDatabase mDbHelper;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
Log.info("[GameProvider] Creating Content Provider...");
|
||||
|
||||
mDbHelper = new GameDatabase(getContext());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
Log.info("[GameProvider] Querying URI: " + uri);
|
||||
|
||||
SQLiteDatabase db = mDbHelper.getReadableDatabase();
|
||||
|
||||
String table = uri.getLastPathSegment();
|
||||
|
||||
if (table == null) {
|
||||
Log.error("[GameProvider] Badly formatted URI: " + uri);
|
||||
return null;
|
||||
}
|
||||
|
||||
Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
Log.verbose("[GameProvider] Getting MIME type for URI: " + uri);
|
||||
String lastSegment = uri.getLastPathSegment();
|
||||
|
||||
if (lastSegment == null) {
|
||||
Log.error("[GameProvider] Badly formatted URI: " + uri);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS)) {
|
||||
return MIME_TYPE_FOLDER;
|
||||
} else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) {
|
||||
return MIME_TYPE_GAME;
|
||||
}
|
||||
|
||||
Log.error("[GameProvider] Unknown MIME type for URI: " + uri);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
Log.info("[GameProvider] Inserting row at URI: " + uri);
|
||||
|
||||
SQLiteDatabase database = mDbHelper.getWritableDatabase();
|
||||
String table = uri.getLastPathSegment();
|
||||
|
||||
if (table != null) {
|
||||
if (table.equals(RESET_LIBRARY)) {
|
||||
mDbHelper.resetDatabase(database);
|
||||
return uri;
|
||||
}
|
||||
if (table.equals(REFRESH_LIBRARY)) {
|
||||
Log.info(
|
||||
"[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents...");
|
||||
mDbHelper.scanLibrary(database);
|
||||
return uri;
|
||||
}
|
||||
|
||||
long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
|
||||
// If insertion was successful...
|
||||
if (id > 0) {
|
||||
// If we just added a folder, add its contents to the game list.
|
||||
if (table.equals(GameDatabase.TABLE_NAME_FOLDERS)) {
|
||||
mDbHelper.scanLibrary(database);
|
||||
}
|
||||
|
||||
// Notify the UI that its contents should be refreshed.
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
uri = Uri.withAppendedPath(uri, Long.toString(id));
|
||||
} else {
|
||||
Log.error("[GameProvider] Row already exists: " + uri + " id: " + id);
|
||||
}
|
||||
} else {
|
||||
Log.error("[GameProvider] Badly formatted URI: " + uri);
|
||||
}
|
||||
|
||||
database.close();
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
||||
Log.error("[GameProvider] Delete operations unsupported. URI: " + uri);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues values, String selection,
|
||||
String[] selectionArgs) {
|
||||
Log.error("[GameProvider] Update operations unsupported. URI: " + uri);
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -1,334 +0,0 @@
|
||||
package org.citra.citra_emu.ui.main;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import java.util.Collections;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.OutOfQuotaPolicy;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkRequest;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
||||
import org.citra.citra_emu.model.GameProvider;
|
||||
import org.citra.citra_emu.ui.platform.PlatformGamesFragment;
|
||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
||||
import org.citra.citra_emu.utils.BillingManager;
|
||||
import org.citra.citra_emu.utils.CiaInstallWorker;
|
||||
import org.citra.citra_emu.utils.CitraDirectoryHelper;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||
import org.citra.citra_emu.utils.InsetsHelper;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
import org.citra.citra_emu.utils.PicassoUtils;
|
||||
import org.citra.citra_emu.utils.StartupHandler;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
/**
|
||||
* The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
|
||||
* individually display a grid of available games for each Fragment, in a tabbed layout.
|
||||
*/
|
||||
public final class MainActivity extends AppCompatActivity implements MainView {
|
||||
private Toolbar mToolbar;
|
||||
private int mFrameLayoutId;
|
||||
private PlatformGamesFragment mPlatformGamesFragment;
|
||||
|
||||
private final MainPresenter mPresenter = new MainPresenter(this);
|
||||
|
||||
// private final CiaInstallWorker mCiaInstallWorker = new CiaInstallWorker();
|
||||
|
||||
// Singleton to manage user billing state
|
||||
private static BillingManager mBillingManager;
|
||||
|
||||
private static MenuItem mPremiumButton;
|
||||
|
||||
private final CitraDirectoryHelper citraDirectoryHelper = new CitraDirectoryHelper(this, () -> {
|
||||
// If mPlatformGamesFragment is null means game directory have not been set yet.
|
||||
if (mPlatformGamesFragment == null) {
|
||||
mPlatformGamesFragment = new PlatformGamesFragment();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(mFrameLayoutId, mPlatformGamesFragment)
|
||||
.commit();
|
||||
showGameInstallDialog();
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Uri> mOpenCitraDirectory =
|
||||
registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
citraDirectoryHelper.showCitraDirectoryDialog(result);
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Uri> mOpenGameListLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
int takeFlags =
|
||||
(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
getContentResolver().takePersistableUriPermission(result, takeFlags);
|
||||
// When a new directory is picked, we currently will reset the existing games
|
||||
// database. This effectively means that only one game directory is supported.
|
||||
// TODO(bunnei): Consider fixing this in the future, or removing code for this.
|
||||
getContentResolver().insert(GameProvider.URI_RESET, null);
|
||||
// Add the new directory
|
||||
mPresenter.onDirectorySelected(result.toString());
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Boolean> mInstallCiaFileLauncher =
|
||||
registerForActivityResult(new OpenFileResultContract(), result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
|
||||
result, getApplicationContext(), Collections.singletonList("cia"));
|
||||
if (selectedFiles == null) {
|
||||
Toast
|
||||
.makeText(getApplicationContext(), R.string.cia_file_not_found,
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
WorkManager workManager = WorkManager.getInstance(getApplicationContext());
|
||||
workManager.enqueueUniqueWork("installCiaWork", ExistingWorkPolicy.APPEND_OR_REPLACE,
|
||||
new OneTimeWorkRequest.Builder(CiaInstallWorker.class)
|
||||
.setInputData(
|
||||
new Data.Builder().putStringArray("CIA_FILES", selectedFiles)
|
||||
.build()
|
||||
)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
);
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<String> requestNotificationPermissionLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { });
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
|
||||
splashScreen.setKeepOnScreenCondition(
|
||||
()
|
||||
-> (PermissionsHandler.hasWriteAccess(this) &&
|
||||
!DirectoryInitialization.areCitraDirectoriesReady()));
|
||||
|
||||
ThemeUtil.applyTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
findViews();
|
||||
|
||||
setSupportActionBar(mToolbar);
|
||||
|
||||
mFrameLayoutId = R.id.games_platform_frame;
|
||||
mPresenter.onCreate();
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
StartupHandler.HandleInit(this, mOpenCitraDirectory);
|
||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
||||
mPlatformGamesFragment = new PlatformGamesFragment();
|
||||
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
|
||||
.commit();
|
||||
}
|
||||
} else {
|
||||
mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
|
||||
}
|
||||
PicassoUtils.init();
|
||||
|
||||
// Setup billing manager, so we can globally query for Premium status
|
||||
mBillingManager = new BillingManager(this);
|
||||
|
||||
// Dismiss previous notifications (should not happen unless a crash occurred)
|
||||
EmulationActivity.tryDismissRunningNotification(this);
|
||||
|
||||
setInsets();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
||||
if (getSupportFragmentManager() == null) {
|
||||
return;
|
||||
}
|
||||
if (outState == null) {
|
||||
return;
|
||||
}
|
||||
getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
mPresenter.addDirIfNeeded(new AddDirectoryHelper(this));
|
||||
|
||||
ThemeUtil.setSystemBarMode(this, ThemeUtil.getIsLightMode(getResources()));
|
||||
}
|
||||
|
||||
// TODO: Replace with a ButterKnife injection.
|
||||
private void findViews() {
|
||||
mToolbar = findViewById(R.id.toolbar_main);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_game_grid, menu);
|
||||
mPremiumButton = menu.findItem(R.id.button_premium);
|
||||
|
||||
if (mBillingManager.isPremiumCached()) {
|
||||
// User had premium in a previous session, hide upsell option
|
||||
setPremiumButtonVisible(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static public void setPremiumButtonVisible(boolean isVisible) {
|
||||
if (mPremiumButton != null) {
|
||||
mPremiumButton.setVisible(isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MainView
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void setVersionString(String version) {
|
||||
mToolbar.setSubtitle(version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
getContentResolver().insert(GameProvider.URI_REFRESH, null);
|
||||
refreshFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchSettingsActivity(String menuTag) {
|
||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
||||
SettingsActivity.launch(this, menuTag, "");
|
||||
} else {
|
||||
PermissionsHandler.checkWritePermission(this, mOpenCitraDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchFileListActivity(int request) {
|
||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
||||
switch (request) {
|
||||
case MainPresenter.REQUEST_SELECT_CITRA_DIRECTORY:
|
||||
mOpenCitraDirectory.launch(null);
|
||||
break;
|
||||
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
||||
mOpenGameListLauncher.launch(null);
|
||||
break;
|
||||
case MainPresenter.REQUEST_INSTALL_CIA:
|
||||
mInstallCiaFileLauncher.launch(true);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
PermissionsHandler.checkWritePermission(this, mOpenCitraDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the framework whenever any actionbar/toolbar icon is clicked.
|
||||
*
|
||||
* @param item The icon that was clicked on.
|
||||
* @return True if the event was handled, false to bubble it up to the OS.
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
return mPresenter.handleOptionSelection(item.getItemId());
|
||||
}
|
||||
|
||||
private void refreshFragment() {
|
||||
if (mPlatformGamesFragment != null) {
|
||||
mPlatformGamesFragment.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void showGameInstallDialog() {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.app_name)
|
||||
.setMessage(R.string.app_game_install_description)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok,
|
||||
(d, v) -> mOpenGameListLauncher.launch(null))
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
EmulationActivity.tryDismissRunningNotification(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if Premium subscription is currently active
|
||||
*/
|
||||
public static boolean isPremiumActive() {
|
||||
return mBillingManager.isPremiumActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the billing flow for Premium
|
||||
*
|
||||
* @param callback Optional callback, called once, on completion of billing
|
||||
*/
|
||||
public static void invokePremiumBilling(Runnable callback) {
|
||||
mBillingManager.invokePremiumBilling(callback);
|
||||
}
|
||||
|
||||
private void setInsets() {
|
||||
AppBarLayout appBar = findViewById(R.id.appbar);
|
||||
FrameLayout frame = findViewById(R.id.games_platform_frame);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
InsetsHelper.insetAppBar(insets, appBar);
|
||||
frame.setPadding(insets.left, 0, insets.right, 0);
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
package org.citra.citra_emu.ui.main;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import org.citra.citra_emu.BuildConfig;
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
public final class MainPresenter {
|
||||
public static final int REQUEST_ADD_DIRECTORY = 1;
|
||||
public static final int REQUEST_INSTALL_CIA = 2;
|
||||
public static final int REQUEST_SELECT_CITRA_DIRECTORY = 3;
|
||||
|
||||
private final MainView mView;
|
||||
private String mDirToAdd;
|
||||
private long mLastClickTime = 0;
|
||||
|
||||
public MainPresenter(MainView view) {
|
||||
mView = view;
|
||||
}
|
||||
|
||||
public void onCreate() {
|
||||
String versionName = BuildConfig.VERSION_NAME;
|
||||
mView.setVersionString(versionName);
|
||||
refreshGameList();
|
||||
}
|
||||
|
||||
public void launchFileListActivity(int request) {
|
||||
if (mView != null) {
|
||||
mView.launchFileListActivity(request);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean handleOptionSelection(int itemId) {
|
||||
// Double-click prevention, using threshold of 500 ms
|
||||
if (SystemClock.elapsedRealtime() - mLastClickTime < 500) {
|
||||
return false;
|
||||
}
|
||||
mLastClickTime = SystemClock.elapsedRealtime();
|
||||
|
||||
switch (itemId) {
|
||||
case R.id.menu_settings_core:
|
||||
mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG);
|
||||
return true;
|
||||
|
||||
case R.id.button_select_root:
|
||||
mView.launchFileListActivity(REQUEST_SELECT_CITRA_DIRECTORY);
|
||||
return true;
|
||||
|
||||
case R.id.button_add_directory:
|
||||
launchFileListActivity(REQUEST_ADD_DIRECTORY);
|
||||
return true;
|
||||
|
||||
case R.id.button_install_cia:
|
||||
launchFileListActivity(REQUEST_INSTALL_CIA);
|
||||
return true;
|
||||
|
||||
case R.id.button_premium:
|
||||
mView.launchSettingsActivity(Settings.SECTION_PREMIUM);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addDirIfNeeded(AddDirectoryHelper helper) {
|
||||
if (mDirToAdd != null) {
|
||||
helper.addDirectory(mDirToAdd, mView::refresh);
|
||||
|
||||
mDirToAdd = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void onDirectorySelected(String dir) {
|
||||
mDirToAdd = dir;
|
||||
}
|
||||
|
||||
public void refreshGameList() {
|
||||
Context context = CitraApplication.getAppContext();
|
||||
if (PermissionsHandler.hasWriteAccess(context)) {
|
||||
GameDatabase databaseHelper = CitraApplication.databaseHelper;
|
||||
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
|
||||
mView.refresh();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package org.citra.citra_emu.ui.main;
|
||||
|
||||
/**
|
||||
* Abstraction for the screen that shows on application launch.
|
||||
* Implementations will differ primarily to target touch-screen
|
||||
* or non-touch screen devices.
|
||||
*/
|
||||
public interface MainView {
|
||||
/**
|
||||
* Pass the view the native library's version string. Displaying
|
||||
* it is optional.
|
||||
*
|
||||
* @param version A string pulled from native code.
|
||||
*/
|
||||
void setVersionString(String version);
|
||||
|
||||
/**
|
||||
* Tell the view to refresh its contents.
|
||||
*/
|
||||
void refresh();
|
||||
|
||||
void launchSettingsActivity(String menuTag);
|
||||
|
||||
void launchFileListActivity(int request);
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
package org.citra.citra_emu.ui.platform;
|
||||
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.adapters.GameAdapter;
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
|
||||
public final class PlatformGamesFragment extends Fragment implements PlatformGamesView {
|
||||
private final PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this);
|
||||
|
||||
private GameAdapter mAdapter;
|
||||
private RecyclerView mRecyclerView;
|
||||
private TextView mTextView;
|
||||
private SwipeRefreshLayout mPullToRefresh;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_grid, container, false);
|
||||
|
||||
findViews(rootView);
|
||||
|
||||
mPresenter.onCreateView();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private void onPullToRefresh() {
|
||||
Runnable onPostRunnable = () -> {
|
||||
updateTextView();
|
||||
mPullToRefresh.setRefreshing(false);
|
||||
};
|
||||
Runnable scanLibraryRunnable = () -> {
|
||||
GameDatabase databaseHelper = CitraApplication.databaseHelper;
|
||||
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
|
||||
mPresenter.refresh();
|
||||
mHandler.post(onPostRunnable);
|
||||
};
|
||||
|
||||
mExecutor.execute(scanLibraryRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
int columns = getResources().getInteger(R.integer.game_grid_columns);
|
||||
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns);
|
||||
mAdapter = new GameAdapter();
|
||||
|
||||
mRecyclerView.setLayoutManager(layoutManager);
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
MaterialDividerItemDecoration divider = new MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL);
|
||||
divider.setLastItemDecorated(false);
|
||||
mRecyclerView.addItemDecoration(divider);
|
||||
|
||||
// Add swipe down to refresh gesture
|
||||
mPullToRefresh.setOnRefreshListener(this::onPullToRefresh);
|
||||
mPullToRefresh.setProgressBackgroundColorSchemeColor(MaterialColors.getColor(mPullToRefresh, R.attr.colorPrimary));
|
||||
mPullToRefresh.setColorSchemeColors(MaterialColors.getColor(mPullToRefresh, R.attr.colorOnPrimary));
|
||||
|
||||
setInsets();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
mPresenter.refresh();
|
||||
updateTextView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showGames(Cursor games) {
|
||||
if (mAdapter != null) {
|
||||
mAdapter.swapCursor(games);
|
||||
}
|
||||
updateTextView();
|
||||
}
|
||||
|
||||
private void updateTextView() {
|
||||
mTextView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void findViews(View root) {
|
||||
mRecyclerView = root.findViewById(R.id.grid_games);
|
||||
mTextView = root.findViewById(R.id.gamelist_empty_text);
|
||||
mPullToRefresh = root.findViewById(R.id.refresh_grid_games);
|
||||
}
|
||||
|
||||
private void setInsets() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(0, 0, 0, insets.bottom);
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package org.citra.citra_emu.ui.platform;
|
||||
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
public final class PlatformGamesPresenter {
|
||||
private final PlatformGamesView mView;
|
||||
|
||||
public PlatformGamesPresenter(PlatformGamesView view) {
|
||||
mView = view;
|
||||
}
|
||||
|
||||
public void onCreateView() {
|
||||
loadGames();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
Log.debug("[PlatformGamesPresenter] : Refreshing...");
|
||||
loadGames();
|
||||
}
|
||||
|
||||
private void loadGames() {
|
||||
Log.debug("[PlatformGamesPresenter] : Loading games...");
|
||||
|
||||
GameDatabase databaseHelper = CitraApplication.databaseHelper;
|
||||
|
||||
databaseHelper.getGames()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(games ->
|
||||
{
|
||||
Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...");
|
||||
|
||||
mView.showGames(games);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package org.citra.citra_emu.ui.platform;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
/**
|
||||
* Abstraction for a screen representing a single platform's games.
|
||||
*/
|
||||
public interface PlatformGamesView {
|
||||
/**
|
||||
* Tell the view to refresh its contents.
|
||||
*/
|
||||
void refresh();
|
||||
|
||||
/**
|
||||
* To be called when an asynchronous database read completes. Passes the
|
||||
* result, in this case a {@link Cursor}, to the view.
|
||||
*
|
||||
* @param games A Cursor containing the games read from the database.
|
||||
*/
|
||||
void showGames(Cursor games);
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.AsyncQueryHandler;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.model.GameProvider;
|
||||
|
||||
public class AddDirectoryHelper {
|
||||
private Context mContext;
|
||||
|
||||
public AddDirectoryHelper(Context context) {
|
||||
this.mContext = context;
|
||||
}
|
||||
|
||||
public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) {
|
||||
AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
|
||||
@Override
|
||||
protected void onInsertComplete(int token, Object cookie, Uri uri) {
|
||||
addDirectoryListener.onDirectoryAdded();
|
||||
}
|
||||
};
|
||||
|
||||
ContentValues file = new ContentValues();
|
||||
file.put(GameDatabase.KEY_FOLDER_PATH, dir);
|
||||
|
||||
handler.startInsert(0, // We don't need to identify this call to the handler
|
||||
null, // We don't need to pass additional data to the handler
|
||||
GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder
|
||||
file);
|
||||
}
|
||||
|
||||
public interface AddDirectoryListener {
|
||||
void onDirectoryAdded();
|
||||
}
|
||||
}
|
@ -1,215 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
||||
import com.android.billingclient.api.BillingClient;
|
||||
import com.android.billingclient.api.BillingClientStateListener;
|
||||
import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.BillingResult;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.Purchase.PurchasesResult;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BillingManager implements PurchasesUpdatedListener {
|
||||
private final String BILLING_SKU_PREMIUM = "citra.citra_emu.product_id.premium";
|
||||
|
||||
private final Activity mActivity;
|
||||
private BillingClient mBillingClient;
|
||||
private SkuDetails mSkuPremium;
|
||||
private boolean mIsPremiumActive = false;
|
||||
private boolean mIsServiceConnected = false;
|
||||
private Runnable mUpdateBillingCallback;
|
||||
|
||||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
|
||||
public BillingManager(Activity activity) {
|
||||
mActivity = activity;
|
||||
mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build();
|
||||
querySkuDetails();
|
||||
}
|
||||
|
||||
static public boolean isPremiumCached() {
|
||||
return mPreferences.getBoolean(SettingsFile.KEY_PREMIUM, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if Premium subscription is currently active
|
||||
*/
|
||||
public boolean isPremiumActive() {
|
||||
return mIsPremiumActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the billing flow for Premium
|
||||
*
|
||||
* @param callback Optional callback, called once, on completion of billing
|
||||
*/
|
||||
public void invokePremiumBilling(Runnable callback) {
|
||||
if (mSkuPremium == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional callback to refresh the UI for the caller when billing completes
|
||||
mUpdateBillingCallback = callback;
|
||||
|
||||
// Invoke the billing flow
|
||||
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(mSkuPremium)
|
||||
.build();
|
||||
mBillingClient.launchBillingFlow(mActivity, flowParams);
|
||||
}
|
||||
|
||||
private void updatePremiumState(boolean isPremiumActive) {
|
||||
mIsPremiumActive = isPremiumActive;
|
||||
|
||||
// Cache state for synchronous UI
|
||||
SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean(SettingsFile.KEY_PREMIUM, isPremiumActive);
|
||||
editor.apply();
|
||||
|
||||
// No need to show button in action bar if Premium is active
|
||||
MainActivity.setPremiumButtonVisible(!isPremiumActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchaseList) {
|
||||
if (purchaseList == null || purchaseList.isEmpty()) {
|
||||
// Premium is not active, or billing is unavailable
|
||||
updatePremiumState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Purchase premiumPurchase = null;
|
||||
for (Purchase purchase : purchaseList) {
|
||||
if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) {
|
||||
premiumPurchase = purchase;
|
||||
}
|
||||
}
|
||||
|
||||
if (premiumPurchase != null && premiumPurchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
|
||||
// Premium has been purchased
|
||||
updatePremiumState(true);
|
||||
|
||||
// Acknowledge the purchase if it hasn't already been acknowledged.
|
||||
if (!premiumPurchase.isAcknowledged()) {
|
||||
AcknowledgePurchaseParams acknowledgePurchaseParams =
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(premiumPurchase.getPurchaseToken())
|
||||
.build();
|
||||
|
||||
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult1 -> {
|
||||
Toast.makeText(mActivity, R.string.premium_settings_welcome, Toast.LENGTH_SHORT).show();
|
||||
};
|
||||
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
|
||||
}
|
||||
|
||||
if (mUpdateBillingCallback != null) {
|
||||
try {
|
||||
mUpdateBillingCallback.run();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
mUpdateBillingCallback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onQuerySkuDetailsFinished(List<SkuDetails> skuDetailsList) {
|
||||
if (skuDetailsList == null) {
|
||||
// This can happen when no user is signed in
|
||||
return;
|
||||
}
|
||||
|
||||
if (skuDetailsList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mSkuPremium = skuDetailsList.get(0);
|
||||
|
||||
queryPurchases();
|
||||
}
|
||||
|
||||
private void querySkuDetails() {
|
||||
Runnable queryToExecute = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
||||
List<String> skuList = new ArrayList<>();
|
||||
|
||||
skuList.add(BILLING_SKU_PREMIUM);
|
||||
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
|
||||
|
||||
mBillingClient.querySkuDetailsAsync(params.build(),
|
||||
(billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList));
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(queryToExecute);
|
||||
}
|
||||
|
||||
private void onQueryPurchasesFinished(PurchasesResult result) {
|
||||
// Have we been disposed of in the meantime? If so, or bad result code, then quit
|
||||
if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
||||
updatePremiumState(false);
|
||||
return;
|
||||
}
|
||||
// Update the UI and purchases inventory with new list of purchases
|
||||
onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList());
|
||||
}
|
||||
|
||||
private void queryPurchases() {
|
||||
Runnable queryToExecute = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
|
||||
onQueryPurchasesFinished(purchasesResult);
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(queryToExecute);
|
||||
}
|
||||
|
||||
private void startServiceConnection(final Runnable executeOnFinish) {
|
||||
mBillingClient.startConnection(new BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResult) {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
mIsServiceConnected = true;
|
||||
}
|
||||
|
||||
if (executeOnFinish != null) {
|
||||
executeOnFinish.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
mIsServiceConnected = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void executeServiceRequest(Runnable runnable) {
|
||||
if (mIsServiceConnected) {
|
||||
runnable.run();
|
||||
} else {
|
||||
// If billing service was disconnected, we try to reconnect 1 time.
|
||||
startServiceConnection(runnable);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.citra.citra_emu.dialogs.CitraDirectoryDialog;
|
||||
import org.citra.citra_emu.dialogs.CopyDirProgressDialog;
|
||||
|
||||
/**
|
||||
* Citra directory initialization ui flow controller.
|
||||
*/
|
||||
public class CitraDirectoryHelper {
|
||||
public interface Listener {
|
||||
void onDirectoryInitialized();
|
||||
}
|
||||
|
||||
private final FragmentActivity mFragmentActivity;
|
||||
private final Listener mListener;
|
||||
|
||||
public CitraDirectoryHelper(FragmentActivity mFragmentActivity, Listener mListener) {
|
||||
this.mFragmentActivity = mFragmentActivity;
|
||||
this.mListener = mListener;
|
||||
}
|
||||
|
||||
public void showCitraDirectoryDialog(Uri result) {
|
||||
CitraDirectoryDialog citraDirectoryDialog = CitraDirectoryDialog.newInstance(
|
||||
result.toString(), ((moveData, path) -> {
|
||||
Uri previous = PermissionsHandler.getCitraDirectory();
|
||||
// Do noting if user select the previous path.
|
||||
if (path.equals(previous)) {
|
||||
return;
|
||||
}
|
||||
int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
mFragmentActivity.getContentResolver().takePersistableUriPermission(path,
|
||||
takeFlags);
|
||||
if (!moveData || previous == null) {
|
||||
initializeCitraDirectory(path);
|
||||
mListener.onDirectoryInitialized();
|
||||
return;
|
||||
}
|
||||
|
||||
// If user check move data, show copy progress dialog.
|
||||
showCopyDialog(previous, path);
|
||||
}));
|
||||
citraDirectoryDialog.show(mFragmentActivity.getSupportFragmentManager(),
|
||||
CitraDirectoryDialog.TAG);
|
||||
}
|
||||
|
||||
private void showCopyDialog(Uri previous, Uri path) {
|
||||
CopyDirProgressDialog copyDirProgressDialog = new CopyDirProgressDialog();
|
||||
copyDirProgressDialog.showNow(mFragmentActivity.getSupportFragmentManager(),
|
||||
CopyDirProgressDialog.TAG);
|
||||
|
||||
// Run copy dir in background
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
FileUtil.copyDir(
|
||||
mFragmentActivity, previous.toString(), path.toString(),
|
||||
new FileUtil.CopyDirListener() {
|
||||
@Override
|
||||
public void onSearchProgress(String directoryName) {
|
||||
copyDirProgressDialog.onUpdateSearchProgress(directoryName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCopyProgress(String filename, int progress, int max) {
|
||||
copyDirProgressDialog.onUpdateCopyProgress(filename, progress, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
initializeCitraDirectory(path);
|
||||
copyDirProgressDialog.dismissAllowingStateLoss();
|
||||
mListener.onDirectoryInitialized();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeCitraDirectory(Uri path) {
|
||||
if (!PermissionsHandler.setCitraDirectory(path.toString()))
|
||||
return;
|
||||
DirectoryInitialization.resetCitraDirectoryState();
|
||||
DirectoryInitialization.start(mFragmentActivity);
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/**
|
||||
* Some controllers have incorrect mappings. This class has special-case fixes for them.
|
||||
*/
|
||||
public class ControllerMappingHelper {
|
||||
/**
|
||||
* Some controllers report extra button presses that can be ignored.
|
||||
*/
|
||||
public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) {
|
||||
if (isDualShock4(inputDevice)) {
|
||||
// The two analog triggers generate analog motion events as well as a keycode.
|
||||
// We always prefer to use the analog values, so throw away the button press
|
||||
return keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale an axis to be zero-centered with a proper range.
|
||||
*/
|
||||
public float scaleAxis(InputDevice inputDevice, int axis, float value) {
|
||||
if (isDualShock4(inputDevice)) {
|
||||
// Android doesn't have correct mappings for this controller's triggers. It reports them
|
||||
// as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
|
||||
// Scale them to properly zero-centered with a range of [0.0, 1.0].
|
||||
if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) {
|
||||
return (value + 1) / 2.0f;
|
||||
}
|
||||
} else if (isXboxOneWireless(inputDevice)) {
|
||||
// Same as the DualShock 4, the mappings are missing.
|
||||
if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) {
|
||||
return (value + 1) / 2.0f;
|
||||
}
|
||||
if (axis == MotionEvent.AXIS_GENERIC_1) {
|
||||
// This axis is stuck at ~.5. Ignore it.
|
||||
return 0.0f;
|
||||
}
|
||||
} else if (isMogaPro2Hid(inputDevice)) {
|
||||
// This controller has a broken axis that reports a constant value. Ignore it.
|
||||
if (axis == MotionEvent.AXIS_GENERIC_1) {
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean isDualShock4(InputDevice inputDevice) {
|
||||
// Sony DualShock 4 controller
|
||||
return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc;
|
||||
}
|
||||
|
||||
private boolean isXboxOneWireless(InputDevice inputDevice) {
|
||||
// Microsoft Xbox One controller
|
||||
return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0;
|
||||
}
|
||||
|
||||
private boolean isMogaPro2Hid(InputDevice inputDevice) {
|
||||
// Moga Pro 2 HID
|
||||
return inputDevice.getVendorId() == 0x20d6 && inputDevice.getProductId() == 0x6271;
|
||||
}
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
/**
|
||||
* Copyright 2014 Dolphin Emulator Project
|
||||
* Licensed under GPLv2+
|
||||
* Refer to the license.txt file included.
|
||||
*/
|
||||
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
|
||||
/**
|
||||
* A service that spawns its own thread in order to copy several binary and shader files
|
||||
* from the Citra APK to the external file system.
|
||||
*/
|
||||
public final class DirectoryInitialization {
|
||||
public static final String BROADCAST_ACTION = "org.citra.citra_emu.BROADCAST";
|
||||
|
||||
public static final String EXTRA_STATE = "directoryState";
|
||||
private static volatile DirectoryInitializationState directoryState = null;
|
||||
private static String userPath;
|
||||
private static AtomicBoolean isCitraDirectoryInitializationRunning = new AtomicBoolean(false);
|
||||
|
||||
public static void start(Context context) {
|
||||
// Can take a few seconds to run, so don't block UI thread.
|
||||
//noinspection TrivialFunctionalExpressionUsage
|
||||
((Runnable) () -> init(context)).run();
|
||||
}
|
||||
|
||||
private static void init(Context context) {
|
||||
if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true))
|
||||
return;
|
||||
|
||||
if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
|
||||
if (PermissionsHandler.hasWriteAccess(context)) {
|
||||
if (setCitraUserDirectory()) {
|
||||
initializeInternalStorage(context);
|
||||
CitraApplication.documentsTree.setRoot(Uri.parse(userPath));
|
||||
NativeLibrary.CreateLogFile();
|
||||
NativeLibrary.LogUserDirectory(userPath);
|
||||
NativeLibrary.CreateConfigFile();
|
||||
directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
|
||||
} else {
|
||||
directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
|
||||
}
|
||||
} else {
|
||||
directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
|
||||
}
|
||||
}
|
||||
|
||||
isCitraDirectoryInitializationRunning.set(false);
|
||||
sendBroadcastState(directoryState, context);
|
||||
}
|
||||
|
||||
private static void deleteDirectoryRecursively(File file) {
|
||||
if (file.isDirectory()) {
|
||||
for (File child : file.listFiles())
|
||||
deleteDirectoryRecursively(child);
|
||||
}
|
||||
file.delete();
|
||||
}
|
||||
|
||||
public static boolean areCitraDirectoriesReady() {
|
||||
return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
|
||||
}
|
||||
|
||||
public static void resetCitraDirectoryState() {
|
||||
directoryState = null;
|
||||
isCitraDirectoryInitializationRunning.compareAndSet(true, false);
|
||||
}
|
||||
|
||||
public static String getUserDirectory() {
|
||||
if (directoryState == null) {
|
||||
throw new IllegalStateException("DirectoryInitialization has to run at least once!");
|
||||
} else if (isCitraDirectoryInitializationRunning.get()) {
|
||||
throw new IllegalStateException(
|
||||
"DirectoryInitialization has to finish running first!");
|
||||
}
|
||||
return userPath;
|
||||
}
|
||||
|
||||
private static native void SetSysDirectory(String path);
|
||||
|
||||
private static boolean setCitraUserDirectory() {
|
||||
Uri dataPath = PermissionsHandler.getCitraDirectory();
|
||||
if (dataPath != null) {
|
||||
userPath = dataPath.toString();
|
||||
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void initializeInternalStorage(Context context) {
|
||||
File sysDirectory = new File(context.getFilesDir(), "Sys");
|
||||
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String revision = NativeLibrary.GetGitRevision();
|
||||
if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) {
|
||||
// There is no extracted Sys directory, or there is a Sys directory from another
|
||||
// version of Citra that might contain outdated files. Let's (re-)extract Sys.
|
||||
deleteDirectoryRecursively(sysDirectory);
|
||||
copyAssetFolder("Sys", sysDirectory, true, context);
|
||||
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString("sysDirectoryVersion", revision);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
// Let the native code know where the Sys directory is.
|
||||
SetSysDirectory(sysDirectory.getPath());
|
||||
}
|
||||
|
||||
private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
|
||||
Intent localIntent =
|
||||
new Intent(BROADCAST_ACTION)
|
||||
.putExtra(EXTRA_STATE, state);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
|
||||
}
|
||||
|
||||
private static void copyAsset(String asset, File output, Boolean overwrite, Context context) {
|
||||
Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
|
||||
|
||||
try {
|
||||
if (!output.exists() || overwrite) {
|
||||
InputStream in = context.getAssets().open(asset);
|
||||
OutputStream out = new FileOutputStream(output);
|
||||
copyFile(in, out);
|
||||
in.close();
|
||||
out.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite,
|
||||
Context context) {
|
||||
Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
|
||||
outputFolder);
|
||||
|
||||
try {
|
||||
boolean createdFolder = false;
|
||||
for (String file : context.getAssets().list(assetFolder)) {
|
||||
if (!createdFolder) {
|
||||
outputFolder.mkdir();
|
||||
createdFolder = true;
|
||||
}
|
||||
copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
|
||||
overwrite, context);
|
||||
copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite,
|
||||
context);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyFile(InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buffer = new byte[1024];
|
||||
int read;
|
||||
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
public enum DirectoryInitializationState {
|
||||
CITRA_DIRECTORIES_INITIALIZED,
|
||||
EXTERNAL_STORAGE_PERMISSION_NEEDED,
|
||||
CANT_FIND_EXTERNAL_STORAGE
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
||||
|
||||
public class DirectoryStateReceiver extends BroadcastReceiver {
|
||||
Action1<DirectoryInitializationState> callback;
|
||||
|
||||
public DirectoryStateReceiver(Action1<DirectoryInitializationState> callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
DirectoryInitializationState state = (DirectoryInitializationState) intent
|
||||
.getSerializableExtra(DirectoryInitialization.EXTRA_STATE);
|
||||
callback.call(state);
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
// Copyright 2021 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class DiskShaderCacheProgress {
|
||||
|
||||
// Equivalent to VideoCore::LoadCallbackStage
|
||||
public enum LoadCallbackStage {
|
||||
Prepare,
|
||||
Decompile,
|
||||
Build,
|
||||
Complete,
|
||||
}
|
||||
|
||||
private static final Object finishLock = new Object();
|
||||
private static ProgressDialogFragment fragment;
|
||||
|
||||
public static class ProgressDialogFragment extends DialogFragment {
|
||||
ProgressBar progressBar;
|
||||
TextView progressText;
|
||||
AlertDialog dialog;
|
||||
|
||||
static ProgressDialogFragment newInstance(String title, String message) {
|
||||
ProgressDialogFragment frag = new ProgressDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("title", title);
|
||||
args.putString("message", message);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity emulationActivity = requireActivity();
|
||||
|
||||
final String title = Objects.requireNonNull(requireArguments().getString("title"));
|
||||
final String message = Objects.requireNonNull(requireArguments().getString("message"));
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(emulationActivity);
|
||||
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
|
||||
|
||||
progressBar = view.findViewById(R.id.progress_bar);
|
||||
progressText = view.findViewById(R.id.progress_text);
|
||||
progressText.setText("");
|
||||
|
||||
setCancelable(false);
|
||||
setRetainInstance(true);
|
||||
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
|
||||
dialog = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setView(view)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> emulationActivity.onBackPressed())
|
||||
.create();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void onUpdateProgress(String msg, int progress, int max) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
progressBar.setProgress(progress);
|
||||
progressBar.setMax(max);
|
||||
progressText.setText(String.format("%d/%d", progress, max));
|
||||
dialog.setMessage(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void prepareDialog() {
|
||||
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
fragment = ProgressDialogFragment.newInstance(emulationActivity.getString(R.string.loading), emulationActivity.getString(R.string.preparing_shaders));
|
||||
fragment.show(emulationActivity.getSupportFragmentManager(), "diskShaders");
|
||||
});
|
||||
|
||||
synchronized (finishLock) {
|
||||
try {
|
||||
finishLock.wait();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadProgress(LoadCallbackStage stage, int progress, int max) {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[DiskShaderCacheProgress] EmulationActivity not present");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (stage) {
|
||||
case Prepare:
|
||||
prepareDialog();
|
||||
break;
|
||||
case Decompile:
|
||||
fragment.onUpdateProgress(emulationActivity.getString(R.string.preparing_shaders), progress, max);
|
||||
break;
|
||||
case Build:
|
||||
fragment.onUpdateProgress(emulationActivity.getString(R.string.building_shaders), progress, max);
|
||||
break;
|
||||
case Complete:
|
||||
// Workaround for when dialog is dismissed when the app is in the background
|
||||
fragment.dismissAllowingStateLoss();
|
||||
|
||||
emulationActivity.runOnUiThread(emulationActivity::onEmulationStarted);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,271 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.model.CheapDocument;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
/**
|
||||
* A cached document tree for citra user directory.
|
||||
* For every filepath which is not startsWith "content://" will need to use this class to traverse.
|
||||
* For example:
|
||||
* C++ citra log file directory will be /log/citra_log.txt.
|
||||
* After DocumentsTree.resolvePath() it will become content URI.
|
||||
*/
|
||||
public class DocumentsTree {
|
||||
private DocumentsNode root;
|
||||
private final Context context;
|
||||
public static final String DELIMITER = "/";
|
||||
|
||||
public DocumentsTree() {
|
||||
context = CitraApplication.getAppContext();
|
||||
}
|
||||
|
||||
public void setRoot(Uri rootUri) {
|
||||
root = null;
|
||||
root = new DocumentsNode();
|
||||
root.uri = rootUri;
|
||||
root.isDirectory = true;
|
||||
}
|
||||
|
||||
public boolean createFile(String filepath, String name) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null) return false;
|
||||
if (!node.isDirectory) return false;
|
||||
if (!node.loaded) structTree(node);
|
||||
Uri mUri = node.uri;
|
||||
try {
|
||||
String filename = URLDecoder.decode(name, FileUtil.DECODE_METHOD);
|
||||
if (node.children.get(filename) != null) return true;
|
||||
DocumentFile createdFile = FileUtil.createFile(context, mUri.toString(), name);
|
||||
if (createdFile == null) return false;
|
||||
DocumentsNode document = new DocumentsNode(createdFile, false);
|
||||
document.parent = node;
|
||||
node.children.put(document.key, document);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[DocumentsTree]: Cannot create file, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean createDir(String filepath, String name) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null) return false;
|
||||
if (!node.isDirectory) return false;
|
||||
if (!node.loaded) structTree(node);
|
||||
Uri mUri = node.uri;
|
||||
try {
|
||||
String filename = URLDecoder.decode(name, FileUtil.DECODE_METHOD);
|
||||
if (node.children.get(filename) != null) return true;
|
||||
DocumentFile createdDirectory = FileUtil.createDir(context, mUri.toString(), name);
|
||||
if (createdDirectory == null) return false;
|
||||
DocumentsNode document = new DocumentsNode(createdDirectory, true);
|
||||
document.parent = node;
|
||||
node.children.put(document.key, document);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[DocumentsTree]: Cannot create file, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int openContentUri(String filepath, String openmode) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null) {
|
||||
return -1;
|
||||
}
|
||||
return FileUtil.openContentUri(context, node.uri.toString(), openmode);
|
||||
}
|
||||
|
||||
public String getFilename(String filepath) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null) {
|
||||
return "";
|
||||
}
|
||||
return node.name;
|
||||
}
|
||||
|
||||
public String[] getFilesName(String filepath) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null || !node.isDirectory) {
|
||||
return new String[0];
|
||||
}
|
||||
// If this directory have not been iterate struct it.
|
||||
if (!node.loaded) structTree(node);
|
||||
return node.children.keySet().toArray(new String[0]);
|
||||
}
|
||||
|
||||
public long getFileSize(String filepath) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null || node.isDirectory) {
|
||||
return 0;
|
||||
}
|
||||
return FileUtil.getFileSize(context, node.uri.toString());
|
||||
}
|
||||
|
||||
public boolean isDirectory(String filepath) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null) return false;
|
||||
return node.isDirectory;
|
||||
}
|
||||
|
||||
public boolean Exists(String filepath) {
|
||||
return resolvePath(filepath) != null;
|
||||
}
|
||||
|
||||
public boolean copyFile(String sourcePath, String destinationParentPath, String destinationFilename) {
|
||||
DocumentsNode sourceNode = resolvePath(sourcePath);
|
||||
if (sourceNode == null) return false;
|
||||
DocumentsNode destinationNode = resolvePath(destinationParentPath);
|
||||
if (destinationNode == null) return false;
|
||||
try {
|
||||
DocumentFile destinationParent = DocumentFile.fromTreeUri(context, destinationNode.uri);
|
||||
if (destinationParent == null) return false;
|
||||
String filename = URLDecoder.decode(destinationFilename, "UTF-8");
|
||||
DocumentFile destination = destinationParent.createFile("application/octet-stream", filename);
|
||||
if (destination == null) return false;
|
||||
DocumentsNode document = new DocumentsNode();
|
||||
document.uri = destination.getUri();
|
||||
document.parent = destinationNode;
|
||||
document.name = destination.getName();
|
||||
document.isDirectory = destination.isDirectory();
|
||||
document.loaded = true;
|
||||
InputStream input = context.getContentResolver().openInputStream(sourceNode.uri);
|
||||
OutputStream output = context.getContentResolver().openOutputStream(destination.getUri(), "wt");
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, len);
|
||||
}
|
||||
input.close();
|
||||
output.flush();
|
||||
output.close();
|
||||
destinationNode.children.put(document.key, document);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[DocumentsTree]: Cannot copy file, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean renameFile(String filepath, String destinationFilename) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null) return false;
|
||||
try {
|
||||
Uri mUri = node.uri;
|
||||
String filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD);
|
||||
DocumentsContract.renameDocument(context.getContentResolver(), mUri, filename);
|
||||
node.rename(filename);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[DocumentsTree]: Cannot rename file, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean deleteDocument(String filepath) {
|
||||
DocumentsNode node = resolvePath(filepath);
|
||||
if (node == null) return false;
|
||||
try {
|
||||
Uri mUri = node.uri;
|
||||
if (!DocumentsContract.deleteDocument(context.getContentResolver(), mUri)) {
|
||||
return false;
|
||||
}
|
||||
if (node.parent != null) {
|
||||
node.parent.children.remove(node.key);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[DocumentsTree]: Cannot rename file, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DocumentsNode resolvePath(String filepath) {
|
||||
if (root == null)
|
||||
return null;
|
||||
StringTokenizer tokens = new StringTokenizer(filepath, DELIMITER, false);
|
||||
DocumentsNode iterator = root;
|
||||
while (tokens.hasMoreTokens()) {
|
||||
String token = tokens.nextToken();
|
||||
if (token.isEmpty()) continue;
|
||||
iterator = find(iterator, token);
|
||||
if (iterator == null) return null;
|
||||
}
|
||||
return iterator;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DocumentsNode find(DocumentsNode parent, String filename) {
|
||||
if (parent.isDirectory && !parent.loaded) {
|
||||
structTree(parent);
|
||||
}
|
||||
return parent.children.get(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct current level directory tree
|
||||
*
|
||||
* @param parent parent node of this level
|
||||
*/
|
||||
private void structTree(DocumentsNode parent) {
|
||||
CheapDocument[] documents = FileUtil.listFiles(context, parent.uri);
|
||||
for (CheapDocument document : documents) {
|
||||
DocumentsNode node = new DocumentsNode(document);
|
||||
node.parent = parent;
|
||||
parent.children.put(node.key, node);
|
||||
}
|
||||
parent.loaded = true;
|
||||
}
|
||||
|
||||
private static class DocumentsNode {
|
||||
private DocumentsNode parent;
|
||||
private final Map<String, DocumentsNode> children = new HashMap<>();
|
||||
private String key;
|
||||
private String name;
|
||||
private Uri uri;
|
||||
private boolean loaded = false;
|
||||
private boolean isDirectory = false;
|
||||
|
||||
private DocumentsNode() {}
|
||||
|
||||
private DocumentsNode(CheapDocument document) {
|
||||
name = document.getFilename();
|
||||
uri = document.getUri();
|
||||
key = FileUtil.getFilenameWithExtensions(uri);
|
||||
isDirectory = document.isDirectory();
|
||||
loaded = !isDirectory;
|
||||
}
|
||||
|
||||
private DocumentsNode(DocumentFile document, boolean isCreateDir) {
|
||||
name = document.getName();
|
||||
uri = document.getUri();
|
||||
key = FileUtil.getFilenameWithExtensions(uri);
|
||||
isDirectory = isCreateDir;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
private void rename(String key) {
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
parent.children.remove(this.key);
|
||||
this.name = key;
|
||||
parent.children.put(key, this);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
|
||||
public class EmulationMenuSettings {
|
||||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
|
||||
// These must match what is defined in src/common/settings.h
|
||||
public static final int LayoutOption_Default = 0;
|
||||
public static final int LayoutOption_SingleScreen = 1;
|
||||
public static final int LayoutOption_LargeScreen = 2;
|
||||
public static final int LayoutOption_SideScreen = 3;
|
||||
public static final int LayoutOption_MobilePortrait = 5;
|
||||
public static final int LayoutOption_MobileLandscape = 6;
|
||||
|
||||
public static boolean getJoystickRelCenter() {
|
||||
return mPreferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true);
|
||||
}
|
||||
|
||||
public static void setJoystickRelCenter(boolean value) {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean("EmulationMenuSettings_JoystickRelCenter", value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean getDpadSlideEnable() {
|
||||
return mPreferences.getBoolean("EmulationMenuSettings_DpadSlideEnable", true);
|
||||
}
|
||||
|
||||
public static void setDpadSlideEnable(boolean value) {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean("EmulationMenuSettings_DpadSlideEnable", value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static int getLandscapeScreenLayout() {
|
||||
return mPreferences.getInt("EmulationMenuSettings_LandscapeScreenLayout", LayoutOption_MobileLandscape);
|
||||
}
|
||||
|
||||
public static void setLandscapeScreenLayout(int value) {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putInt("EmulationMenuSettings_LandscapeScreenLayout", value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean getShowFps() {
|
||||
return mPreferences.getBoolean("EmulationMenuSettings_ShowFps", false);
|
||||
}
|
||||
|
||||
public static void setShowFps(boolean value) {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean("EmulationMenuSettings_ShowFps", value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean getSwapScreens() {
|
||||
return mPreferences.getBoolean("EmulationMenuSettings_SwapScreens", false);
|
||||
}
|
||||
|
||||
public static void setSwapScreens(boolean value) {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean("EmulationMenuSettings_SwapScreens", value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean getShowOverlay() {
|
||||
return mPreferences.getBoolean("EmulationMenuSettings_ShowOverylay", true);
|
||||
}
|
||||
|
||||
public static void setShowOverlay(boolean value) {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean("EmulationMenuSettings_ShowOverylay", value);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
@ -1,464 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.system.Os;
|
||||
import android.system.StructStatVfs;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.citra.citra_emu.model.CheapDocument;
|
||||
|
||||
public class FileUtil {
|
||||
static final String PATH_TREE = "tree";
|
||||
static final String DECODE_METHOD = "UTF-8";
|
||||
static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
|
||||
static final String TEXT_PLAIN = "text/plain";
|
||||
|
||||
public interface CopyDirListener {
|
||||
void onSearchProgress(String directoryName);
|
||||
void onCopyProgress(String filename, int progress, int max);
|
||||
|
||||
void onComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file from directory with filename.
|
||||
*
|
||||
* @param context Application context
|
||||
* @param directory parent path for file.
|
||||
* @param filename file display name.
|
||||
* @return boolean
|
||||
*/
|
||||
@Nullable
|
||||
public static DocumentFile createFile(Context context, String directory, String filename) {
|
||||
try {
|
||||
Uri directoryUri = Uri.parse(directory);
|
||||
DocumentFile parent;
|
||||
parent = DocumentFile.fromTreeUri(context, directoryUri);
|
||||
if (parent == null) return null;
|
||||
filename = URLDecoder.decode(filename, DECODE_METHOD);
|
||||
int extensionPosition = filename.lastIndexOf('.');
|
||||
String extension = "";
|
||||
if (extensionPosition > 0) {
|
||||
extension = filename.substring(extensionPosition);
|
||||
}
|
||||
String mimeType = APPLICATION_OCTET_STREAM;
|
||||
if (extension.equals(".txt")) {
|
||||
mimeType = TEXT_PLAIN;
|
||||
}
|
||||
DocumentFile isExist = parent.findFile(filename);
|
||||
if (isExist != null) return isExist;
|
||||
return parent.createFile(mimeType, filename);
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory from directory with filename.
|
||||
*
|
||||
* @param context Application context
|
||||
* @param directory parent path for directory.
|
||||
* @param directoryName directory display name.
|
||||
* @return boolean
|
||||
*/
|
||||
@Nullable
|
||||
public static DocumentFile createDir(Context context, String directory, String directoryName) {
|
||||
try {
|
||||
Uri directoryUri = Uri.parse(directory);
|
||||
DocumentFile parent;
|
||||
parent = DocumentFile.fromTreeUri(context, directoryUri);
|
||||
if (parent == null) return null;
|
||||
directoryName = URLDecoder.decode(directoryName, DECODE_METHOD);
|
||||
DocumentFile isExist = parent.findFile(directoryName);
|
||||
if (isExist != null) return isExist;
|
||||
return parent.createDirectory(directoryName);
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open content uri and return file descriptor to JNI.
|
||||
*
|
||||
* @param context Application context
|
||||
* @param path Native content uri path
|
||||
* @param openmode will be one of "r", "r", "rw", "wa", "rwa"
|
||||
* @return file descriptor
|
||||
*/
|
||||
public static int openContentUri(Context context, String path, String openmode) {
|
||||
try (ParcelFileDescriptor parcelFileDescriptor =
|
||||
context.getContentResolver().openFileDescriptor(Uri.parse(path), openmode)) {
|
||||
if (parcelFileDescriptor == null) {
|
||||
Log.error("[FileUtil]: Cannot get the file descriptor from uri: " + path);
|
||||
return -1;
|
||||
}
|
||||
return parcelFileDescriptor.detachFd();
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot open content uri, error: " + e.getMessage());
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
|
||||
* This function will be faster than DocumentFile.listFiles
|
||||
*
|
||||
* @param context Application context
|
||||
* @param uri Directory uri.
|
||||
* @return CheapDocument lists.
|
||||
*/
|
||||
public static CheapDocument[] listFiles(Context context, Uri uri) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final String[] columns = new String[]{
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
};
|
||||
Cursor c = null;
|
||||
final List<CheapDocument> results = new ArrayList<>();
|
||||
try {
|
||||
String docId;
|
||||
if (isRootTreeUri(uri)) {
|
||||
docId = DocumentsContract.getTreeDocumentId(uri);
|
||||
} else {
|
||||
docId = DocumentsContract.getDocumentId(uri);
|
||||
}
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId);
|
||||
c = resolver.query(childrenUri, columns, null, null, null);
|
||||
while (c.moveToNext()) {
|
||||
final String documentId = c.getString(0);
|
||||
final String documentName = c.getString(1);
|
||||
final String documentMimeType = c.getString(2);
|
||||
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
|
||||
CheapDocument document = new CheapDocument(documentName, documentMimeType, documentUri);
|
||||
results.add(document);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot list file error: " + e.getMessage());
|
||||
} finally {
|
||||
closeQuietly(c);
|
||||
}
|
||||
return results.toArray(new CheapDocument[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given path exists.
|
||||
*
|
||||
* @param path Native content uri path
|
||||
* @return bool
|
||||
*/
|
||||
public static boolean Exists(Context context, String path) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri mUri = Uri.parse(path);
|
||||
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_DOCUMENT_ID};
|
||||
c = context.getContentResolver().query(mUri, columns, null, null, null);
|
||||
return c.getCount() > 0;
|
||||
} catch (Exception e) {
|
||||
Log.info("[FileUtil] Cannot find file from given path, error: " + e.getMessage());
|
||||
} finally {
|
||||
closeQuietly(c);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given path is a directory
|
||||
*
|
||||
* @param path content uri path
|
||||
* @return bool
|
||||
*/
|
||||
public static boolean isDirectory(Context context, String path) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_MIME_TYPE};
|
||||
boolean isDirectory = false;
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri mUri = Uri.parse(path);
|
||||
c = resolver.query(mUri, columns, null, null, null);
|
||||
c.moveToNext();
|
||||
final String mimeType = c.getString(0);
|
||||
isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot list files, error: " + e.getMessage());
|
||||
} finally {
|
||||
closeQuietly(c);
|
||||
}
|
||||
return isDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file display name from given path
|
||||
*
|
||||
* @param path content uri path
|
||||
* @return String display name
|
||||
*/
|
||||
public static String getFilename(Context context, String path) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_DISPLAY_NAME};
|
||||
String filename = "";
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri mUri = Uri.parse(path);
|
||||
c = resolver.query(mUri, columns, null, null, null);
|
||||
c.moveToNext();
|
||||
filename = c.getString(0);
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
|
||||
} finally {
|
||||
closeQuietly(c);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
public static String[] getFilesName(Context context, String path) {
|
||||
Uri uri = Uri.parse(path);
|
||||
List<String> files = new ArrayList<>();
|
||||
for (CheapDocument file : FileUtil.listFiles(context, uri)) {
|
||||
files.add(file.getFilename());
|
||||
}
|
||||
return files.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file size from given path.
|
||||
*
|
||||
* @param path content uri path
|
||||
* @return long file size
|
||||
*/
|
||||
public static long getFileSize(Context context, String path) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_SIZE};
|
||||
long size = 0;
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri mUri = Uri.parse(path);
|
||||
c = resolver.query(mUri, columns, null, null, null);
|
||||
c.moveToNext();
|
||||
size = c.getLong(0);
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
|
||||
} finally {
|
||||
closeQuietly(c);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
public static boolean copyFile(Context context, String sourcePath, String destinationParentPath, String destinationFilename) {
|
||||
try {
|
||||
Uri sourceUri = Uri.parse(sourcePath);
|
||||
Uri destinationUri = Uri.parse(destinationParentPath);
|
||||
DocumentFile destinationParent = DocumentFile.fromTreeUri(context, destinationUri);
|
||||
if (destinationParent == null) return false;
|
||||
String filename = URLDecoder.decode(destinationFilename, "UTF-8");
|
||||
DocumentFile destination = destinationParent.findFile(filename);
|
||||
if (destination == null) {
|
||||
destination = destinationParent.createFile("application/octet-stream", filename);
|
||||
}
|
||||
if (destination == null) return false;
|
||||
InputStream input = context.getContentResolver().openInputStream(sourceUri);
|
||||
OutputStream output = context.getContentResolver().openOutputStream(destination.getUri(), "wt");
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, len);
|
||||
}
|
||||
input.close();
|
||||
output.flush();
|
||||
output.close();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot copy file, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void copyDir(Context context, String sourcePath, String destinationPath,
|
||||
CopyDirListener listener) {
|
||||
try {
|
||||
Uri sourceUri = Uri.parse(sourcePath);
|
||||
Uri destinationUri = Uri.parse(destinationPath);
|
||||
final List<Pair<CheapDocument, DocumentFile>> files = new ArrayList<>();
|
||||
final List<Pair<Uri, Uri>> dirs = new ArrayList<>();
|
||||
dirs.add(new Pair<>(sourceUri, destinationUri));
|
||||
// Searching all files which need to be copied and struct the directory in destination.
|
||||
while (!dirs.isEmpty()) {
|
||||
DocumentFile fromDir = DocumentFile.fromTreeUri(context, dirs.get(0).first);
|
||||
DocumentFile toDir = DocumentFile.fromTreeUri(context, dirs.get(0).second);
|
||||
if (fromDir == null || toDir == null)
|
||||
continue;
|
||||
Uri fromUri = fromDir.getUri();
|
||||
if (listener != null) {
|
||||
listener.onSearchProgress(fromUri.getPath());
|
||||
}
|
||||
CheapDocument[] documents = FileUtil.listFiles(context, fromUri);
|
||||
for (CheapDocument document : documents) {
|
||||
String filename = document.getFilename();
|
||||
if (document.isDirectory()) {
|
||||
DocumentFile target = toDir.findFile(filename);
|
||||
if (target == null || !target.exists()) {
|
||||
target = toDir.createDirectory(filename);
|
||||
}
|
||||
if (target == null)
|
||||
continue;
|
||||
dirs.add(new Pair<>(document.getUri(), target.getUri()));
|
||||
} else {
|
||||
DocumentFile target = toDir.findFile(filename);
|
||||
if (target == null || !target.exists()) {
|
||||
target =
|
||||
toDir.createFile(document.getMimeType(), document.getFilename());
|
||||
}
|
||||
if (target == null)
|
||||
continue;
|
||||
files.add(new Pair<>(document, target));
|
||||
}
|
||||
}
|
||||
|
||||
dirs.remove(0);
|
||||
}
|
||||
|
||||
int total = files.size();
|
||||
int progress = 0;
|
||||
for (Pair<CheapDocument, DocumentFile> file : files) {
|
||||
DocumentFile to = file.second;
|
||||
Uri toUri = to.getUri();
|
||||
String filename = getFilenameWithExtensions(toUri);
|
||||
String toPath = toUri.getPath();
|
||||
DocumentFile toParent = to.getParentFile();
|
||||
if (toParent == null)
|
||||
continue;
|
||||
FileUtil.copyFile(context, file.first.getUri().toString(),
|
||||
toParent.getUri().toString(), filename);
|
||||
progress++;
|
||||
if (listener != null) {
|
||||
listener.onCopyProgress(toPath, progress, total);
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onComplete();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot copy directory, error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean renameFile(Context context, String path, String destinationFilename) {
|
||||
try {
|
||||
Uri uri = Uri.parse(path);
|
||||
DocumentsContract.renameDocument(context.getContentResolver(), uri, destinationFilename);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot rename file, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean deleteDocument(Context context, String path) {
|
||||
try {
|
||||
Uri uri = Uri.parse(path);
|
||||
DocumentsContract.deleteDocument(context.getContentResolver(), uri);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil]: Cannot delete document, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static byte[] getBytesFromFile(Context context, DocumentFile file) throws IOException {
|
||||
final Uri uri = file.getUri();
|
||||
final long length = FileUtil.getFileSize(context, uri.toString());
|
||||
|
||||
// You cannot create an array using a long type.
|
||||
if (length > Integer.MAX_VALUE) {
|
||||
// File is too large
|
||||
throw new IOException("File is too large!");
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[(int) length];
|
||||
|
||||
int offset = 0;
|
||||
int numRead;
|
||||
|
||||
try (InputStream is = context.getContentResolver().openInputStream(uri)) {
|
||||
while (offset < bytes.length &&
|
||||
(numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
|
||||
offset += numRead;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all the bytes have been read in
|
||||
if (offset < bytes.length) {
|
||||
throw new IOException("Could not completely read file " + file.getName());
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static boolean isRootTreeUri(Uri uri) {
|
||||
final List<String> paths = uri.getPathSegments();
|
||||
return paths.size() == 2 && PATH_TREE.equals(paths.get(0));
|
||||
}
|
||||
|
||||
public static boolean isNativePath(String path) {
|
||||
try {
|
||||
return path.charAt(0) == '/';
|
||||
} catch (StringIndexOutOfBoundsException e) {
|
||||
Log.error("[FileUtil] Cannot determine the string is native path or not.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String getFilenameWithExtensions(Uri uri) {
|
||||
String path = uri.getPath();
|
||||
final int slashIndex = path.lastIndexOf('/');
|
||||
path = path.substring(slashIndex + 1);
|
||||
// On Android versions below 10, it is possible to select the storage root, which might result in filenames with a colon.
|
||||
final int colonIndex = path.indexOf(':');
|
||||
return path.substring(colonIndex + 1);
|
||||
}
|
||||
|
||||
public static double getFreeSpace(Context context, Uri uri) {
|
||||
try {
|
||||
Uri docTreeUri = DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri, DocumentsContract.getTreeDocumentId(uri));
|
||||
ParcelFileDescriptor pfd =
|
||||
context.getContentResolver().openFileDescriptor(docTreeUri, "r");
|
||||
assert pfd != null;
|
||||
StructStatVfs stats = Os.fstatvfs(pfd.getFileDescriptor());
|
||||
double spaceInGigaBytes = stats.f_bavail * stats.f_bsize / 1024.0 / 1024 / 1024;
|
||||
pfd.close();
|
||||
return spaceInGigaBytes;
|
||||
} catch (Exception e) {
|
||||
Log.error("[FileUtil] Cannot get storage size.");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static void closeQuietly(AutoCloseable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Copyright 2014 Dolphin Emulator Project
|
||||
* Licensed under GPLv2+
|
||||
* Refer to the license.txt file included.
|
||||
*/
|
||||
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
|
||||
/**
|
||||
* A service that shows a permanent notification in the background to avoid the app getting
|
||||
* cleared from memory by the system.
|
||||
*/
|
||||
public class ForegroundService extends Service {
|
||||
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
||||
|
||||
private void showRunningNotification() {
|
||||
// Intent is used to resume emulation if the notification is clicked
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
|
||||
new Intent(this, EmulationActivity.class), PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
|
||||
.setSmallIcon(R.drawable.ic_stat_notification_logo)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(getString(R.string.app_notification_running))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.setVibrate(null)
|
||||
.setSound(null)
|
||||
.setContentIntent(contentIntent);
|
||||
startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
showRunningNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION);
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Request;
|
||||
import com.squareup.picasso.RequestHandler;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.model.GameInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
public class GameIconRequestHandler extends RequestHandler {
|
||||
@Override
|
||||
public boolean canHandleRequest(Request data) {
|
||||
return "content".equals(data.uri.getScheme()) || data.uri.getScheme() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result load(Request request, int networkPolicy) {
|
||||
int[] vector;
|
||||
try {
|
||||
String url = request.uri.toString();
|
||||
vector = new GameInfo(url).getIcon();
|
||||
} catch (IOException e) {
|
||||
vector = null;
|
||||
}
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565);
|
||||
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector));
|
||||
return new Result(bitmap, Picasso.LoadedFrom.DISK);
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
|
||||
public class PermissionsHandler {
|
||||
public static final String CITRA_DIRECTORY = "CITRA_DIRECTORY";
|
||||
public static final SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
|
||||
// We use permissions acceptance as an indicator if this is a first boot for the user.
|
||||
public static boolean isFirstBoot(FragmentActivity activity) {
|
||||
return !hasWriteAccess(activity.getApplicationContext());
|
||||
}
|
||||
|
||||
public static boolean checkWritePermission(FragmentActivity activity,
|
||||
ActivityResultLauncher<Uri> launcher) {
|
||||
if (isFirstBoot(activity)) {
|
||||
launcher.launch(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean hasWriteAccess(Context context) {
|
||||
try {
|
||||
Uri uri = getCitraDirectory();
|
||||
if (uri == null)
|
||||
return false;
|
||||
int takeFlags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
|
||||
DocumentFile root = DocumentFile.fromTreeUri(context, uri);
|
||||
if (root != null && root.exists()) return true;
|
||||
context.getContentResolver().releasePersistableUriPermission(uri, takeFlags);
|
||||
} catch (Exception e) {
|
||||
Log.error("[PermissionsHandler]: Cannot check citra data directory permission, error: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Uri getCitraDirectory() {
|
||||
String directoryString = mPreferences.getString(CITRA_DIRECTORY, "");
|
||||
if (directoryString.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Uri.parse(directoryString);
|
||||
}
|
||||
|
||||
public static boolean setCitraDirectory(String uriString) {
|
||||
return mPreferences.edit().putString(CITRA_DIRECTORY, uriString).commit();
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapShader;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import com.squareup.picasso.Transformation;
|
||||
|
||||
public class PicassoRoundedCornersTransformation implements Transformation {
|
||||
@Override
|
||||
public Bitmap transform(Bitmap icon) {
|
||||
final int width = icon.getWidth();
|
||||
final int height = icon.getHeight();
|
||||
final Rect rect = new Rect(0, 0, width, height);
|
||||
final int size = Math.min(width, height);
|
||||
final int x = (width - size) / 2;
|
||||
final int y = (height - size) / 2;
|
||||
|
||||
Bitmap squaredBitmap = Bitmap.createBitmap(icon, x, y, size, size);
|
||||
if (squaredBitmap != icon) {
|
||||
icon.recycle();
|
||||
}
|
||||
|
||||
Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(output);
|
||||
BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setShader(shader);
|
||||
|
||||
canvas.drawRoundRect(new RectF(rect), 10, 10, paint);
|
||||
|
||||
squaredBitmap.recycle();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return "circle";
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
|
||||
public final class StartupHandler {
|
||||
private static void handlePermissionsCheck(FragmentActivity parent,
|
||||
ActivityResultLauncher<Uri> launcher) {
|
||||
// Ask the user to grant write permission if it's not already granted
|
||||
PermissionsHandler.checkWritePermission(parent, launcher);
|
||||
|
||||
String start_file = "";
|
||||
Bundle extras = parent.getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
start_file = extras.getString("AutoStartFile");
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(start_file)) {
|
||||
// Start the emulation activity, send the ISO passed in and finish the main activity
|
||||
Intent emulation_intent = new Intent(parent, EmulationActivity.class);
|
||||
emulation_intent.putExtra("SelectedGame", start_file);
|
||||
parent.startActivity(emulation_intent);
|
||||
parent.finish();
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleInit(FragmentActivity parent, ActivityResultLauncher<Uri> launcher) {
|
||||
if (PermissionsHandler.isFirstBoot(parent)) {
|
||||
// Prompt user with standard first boot disclaimer
|
||||
AlertDialog dialog =
|
||||
new MaterialAlertDialogBuilder(parent)
|
||||
.setTitle(R.string.app_name)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setMessage(R.string.app_disclaimer)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setCancelable(false)
|
||||
.setOnDismissListener(
|
||||
dialogInterface -> handlePermissionsCheck(parent, launcher))
|
||||
.show();
|
||||
TextView textView = dialog.findViewById(android.R.id.message);
|
||||
if (textView == null)
|
||||
return;
|
||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
|
||||
public class ThemeUtil {
|
||||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
|
||||
public static final float NAV_BAR_ALPHA = 0.9f;
|
||||
|
||||
private static void applyTheme(int designValue, AppCompatActivity activity) {
|
||||
switch (designValue) {
|
||||
case 0:
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
break;
|
||||
case 1:
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
break;
|
||||
case 2:
|
||||
AppCompatDelegate.setDefaultNightMode(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ?
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM :
|
||||
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
|
||||
break;
|
||||
}
|
||||
|
||||
setSystemBarMode(activity, getIsLightMode(activity.getResources()));
|
||||
setNavigationBarColor(activity, MaterialColors.getColor(activity.getWindow().getDecorView(), R.attr.colorSurface));
|
||||
}
|
||||
|
||||
public static void applyTheme(AppCompatActivity activity) {
|
||||
applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0), activity);
|
||||
}
|
||||
|
||||
public static void setSystemBarMode(AppCompatActivity activity, boolean isLightMode) {
|
||||
WindowInsetsControllerCompat windowController = WindowCompat.getInsetsController(activity.getWindow(), activity.getWindow().getDecorView());
|
||||
windowController.setAppearanceLightStatusBars(isLightMode);
|
||||
windowController.setAppearanceLightNavigationBars(isLightMode);
|
||||
}
|
||||
|
||||
public static void setNavigationBarColor(@NonNull Activity activity, @ColorInt int color) {
|
||||
int gestureType = InsetsHelper.getSystemGestureType(activity.getApplicationContext());
|
||||
int orientation = activity.getResources().getConfiguration().orientation;
|
||||
|
||||
// Use a solid color when the navigation bar is on the left/right edge of the screen
|
||||
if ((gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||
|
||||
gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION) &&
|
||||
orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
activity.getWindow().setNavigationBarColor(color);
|
||||
} else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||
|
||||
gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION) {
|
||||
// Use semi-transparent color when in portrait mode with three/two button navigation to
|
||||
// partially see list items behind the navigation bar
|
||||
activity.getWindow().setNavigationBarColor(ThemeUtil.getColorWithOpacity(color, NAV_BAR_ALPHA));
|
||||
} else {
|
||||
// Use transparent color when using gesture navigation
|
||||
activity.getWindow().setNavigationBarColor(
|
||||
ContextCompat.getColor(activity.getApplicationContext(),
|
||||
android.R.color.transparent));
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public static int getColorWithOpacity(@ColorInt int color, float alphaFactor) {
|
||||
return Color.argb(Math.round(alphaFactor * Color.alpha(color)), Color.red(color),
|
||||
Color.green(color), Color.blue(color));
|
||||
}
|
||||
|
||||
public static boolean getIsLightMode(Resources resources) {
|
||||
return (resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_NO;
|
||||
}
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
// Copyright 2019 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
// Function calls from the Java side
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_PauseEmulation(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_StopEmulation(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_IsRunning(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent(
|
||||
JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action);
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent(
|
||||
JNIEnv* env, jclass clazz, jstring j_device, jint axis, jfloat x, jfloat y);
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxisEvent(
|
||||
JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val);
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jfloat x, jfloat y,
|
||||
jboolean pressed);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved(JNIEnv* env,
|
||||
jclass clazz, jfloat x,
|
||||
jfloat y);
|
||||
|
||||
JNIEXPORT jintArray JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetIcon(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring j_file);
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetTitle(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring j_filename);
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetDescription(
|
||||
JNIEnv* env, jclass clazz, jstring j_filename);
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetGameId(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring j_filename);
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetRegions(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring j_filename);
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetCompany(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring j_filename);
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetUserDirectory(
|
||||
JNIEnv* env, jclass clazz, jstring j_directory);
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_org_citra_citra_1emu_NativeLibrary_GetInstalledGamePaths(JNIEnv* env, jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_utils_DirectoryInitialization_SetSysDirectory(
|
||||
JNIEnv* env, jclass clazz, jstring path_);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring path);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateLogFile(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LogUserDirectory(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring path);
|
||||
|
||||
JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
|
||||
jclass clazz);
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetProfiling(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jboolean enable);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_NotifyOrientationChange(
|
||||
JNIEnv* env, jclass clazz, jint layout_option, jint rotation);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SwapScreens(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jboolean swap_screens,
|
||||
jint rotation);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2(
|
||||
JNIEnv* env, jclass clazz, jstring j_path);
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z(
|
||||
JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jobject surf);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_InitGameIni(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring j_game_id);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_ReloadSettings(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetUserSetting(
|
||||
JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key,
|
||||
jstring j_value);
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetUserSetting(
|
||||
JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key);
|
||||
|
||||
JNIEXPORT jdoubleArray JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetPerfStats(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_ReloadCameraDevices(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv* env, jclass clazz,
|
||||
jstring j_file);
|
||||
|
||||
JNIEXPORT void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv* env, jclass clazz);
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo(JNIEnv* env, jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SaveState(JNIEnv* env, jclass clazz,
|
||||
jint slot);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LoadState(JNIEnv* env, jclass clazz,
|
||||
jint slot);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_NativeLibrary_InstallCIA(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring file);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refresh_grid_games"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gamelist_empty_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/empty_gamelist"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/grid_games"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/card_game" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</FrameLayout>
|
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp"
|
||||
android:gravity="center">
|
||||
|
||||
<DatePicker
|
||||
android:id="@+id/date_picker"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:calendarViewShown="false"
|
||||
android:datePickerMode="spinner"
|
||||
android:spinnersShown="true" />
|
||||
|
||||
<TimePicker
|
||||
android:id="@+id/time_picker"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:timePickerMode="spinner" />
|
||||
</LinearLayout>
|
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/button_premium"
|
||||
android:icon="@drawable/ic_heart"
|
||||
android:title="@string/premium_text"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/button_file_menu"
|
||||
android:icon="@drawable/ic_folder"
|
||||
android:title="@string/select_game_folder"
|
||||
app:showAsAction="ifRoom">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/button_select_root"
|
||||
android:icon="@drawable/ic_folder"
|
||||
android:title="@string/select_citra_user_folder"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/button_add_directory"
|
||||
android:icon="@drawable/ic_folder"
|
||||
android:title="@string/select_game_folder"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/button_install_cia"
|
||||
android:icon="@drawable/ic_install"
|
||||
android:title="@string/install_cia_title"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/menu_settings_core"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/grid_menu_core_settings"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="game_grid_columns">4</integer>
|
||||
</resources>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||
(such as screen margins) for screens with more than 1024dp of available width. -->
|
||||
<dimen name="activity_horizontal_margin">96dp</dimen>
|
||||
</resources>
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="game_grid_columns">2</integer>
|
||||
</resources>
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="game_grid_columns">3</integer>
|
||||
</resources>
|
@ -1,5 +0,0 @@
|
||||
<resources>
|
||||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||
(such as screen margins) for screens with more than 820dp of available width. -->
|
||||
<dimen name="activity_horizontal_margin">64dp</dimen>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user