(jroweboy) Remove existing code in src/android

Move src/citra_android to src/android/app/src/main/jni
Disable gdbstub breakpoints on android (could be done better)
Disable LOD_BIAS for GLES (not support on gles)
This commit is contained in:
bunnei 2019-07-17 20:00:17 -04:00
parent 7b54e7dfc7
commit 99a2391d05
359 changed files with 11670 additions and 447 deletions

View File

@ -103,10 +103,6 @@ add_subdirectory(network)
add_subdirectory(input_common)
add_subdirectory(tests)
if(ANDROID)
add_subdirectory(citra_android/jni)
endif()
if (ENABLE_SDL2)
add_subdirectory(citra)
endif()
@ -114,8 +110,10 @@ endif()
if (ENABLE_QT)
add_subdirectory(citra_qt)
endif()
if (ANDROID)
add_subdirectory(android/app/src/main/cpp)
include_directories(android/app/src/main)
add_subdirectory(android/app/src/main/jni)
else()
add_subdirectory(dedicated_room)
endif()

View File

@ -1,10 +1,59 @@
# Built application files
*.apk
*.ap_
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
/build
/captures
.idea/
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Google Services (e.g. APIs or Firebase)
google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

View File

@ -0,0 +1,44 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )

View File

@ -1,4 +1,4 @@
package org.citra_emu.citra;
package org.citra.citra_android;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("org.citra_emu.citra_android", appContext.getPackageName());
assertEquals("org.citra.citra_android", appContext.getPackageName());
}
}

View File

@ -1,39 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.citra_emu.citra">
package="org.citra.citra_android">
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature
android:name="android.hardware.gamepad"
android:required="false"/>
<uses-feature
android:name="android.software.leanback"
android:required="false"/>
<uses-feature android:glEsVersion="0x00030001" />
<uses-feature android:glEsVersion="0x00030000" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name="org.citra_emu.citra.CitraApplication"
android:label="Citra"
android:icon="@mipmap/ic_citra"
android:name=".DolphinApplication"
android:label="@string/app_name"
android:icon="@drawable/ic_citra"
android:allowBackup="true"
android:supportsRtl="true"
android:isGame="true"
android:banner="@mipmap/ic_citra">
android:banner="@drawable/ic_citra">
<activity
android:name=".ui.main.MainActivity"
android:theme="@style/CitraBase">
android:theme="@style/Citra">
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ui.main.TvMainActivity"
android:theme="@style/CitraTv">
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ui.settings.SettingsActivity"
android:theme="@style/CitraSettings"
android:label="@string/preferences_settings"/>
<activity
android:name=".activities.EmulationActivity"
android:theme="@style/CitraEmulation"/>
<activity
android:name=".activities.CustomFilePickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service android:name=".services.DirectoryInitializationService"/>
<provider
android:name=".model.GameProvider"
android:authorities="${applicationId}.provider"
android:enabled="true"
android:exported="false">
</provider>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.filesprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/nnf_provider_paths" />
</provider>
</application>
</manifest>

View File

@ -1,16 +0,0 @@
cmake_minimum_required(VERSION 3.8)
add_library(citra-android SHARED
logging/log.cpp
logging/logcat_backend.cpp
logging/logcat_backend.h
native_interface.cpp
native_interface.h
ui/main/main_activity.cpp
)
# find Android's log library
find_library(log-lib log)
target_link_libraries(citra-android ${log-lib} core common inih)
target_include_directories(citra-android PRIVATE "../../../../../" "./")

View File

@ -1,15 +0,0 @@
#include "common/logging/log.h"
#include "native_interface.h"
namespace Log {
extern "C" {
JNICALL void Java_org_citra_1emu_citra_LOG_logEntry(JNIEnv* env, jclass type, jint level,
jstring file_name, jint line_number,
jstring function, jstring msg) {
using CitraJNI::GetJString;
FmtLogMessage(Class::Frontend, static_cast<Level>(level), GetJString(env, file_name).data(),
static_cast<unsigned int>(line_number), GetJString(env, function).data(),
GetJString(env, msg).data());
}
}
} // namespace Log

View File

@ -1,38 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <android/log.h>
#include "common/assert.h"
#include "common/logging/text_formatter.h"
#include "logcat_backend.h"
namespace Log {
void LogcatBackend::Write(const Entry& entry) {
android_LogPriority priority;
switch (entry.log_level) {
case Level::Trace:
priority = ANDROID_LOG_VERBOSE;
break;
case Level::Debug:
priority = ANDROID_LOG_DEBUG;
break;
case Level::Info:
priority = ANDROID_LOG_INFO;
break;
case Level::Warning:
priority = ANDROID_LOG_WARN;
break;
case Level::Error:
priority = ANDROID_LOG_ERROR;
break;
case Level::Critical:
priority = ANDROID_LOG_FATAL;
break;
case Level::Count:
UNREACHABLE();
}
__android_log_print(priority, "citra", "%s\n", FormatLogMessage(entry).c_str());
}
} // namespace Log

View File

@ -1,22 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include "common/logging/backend.h"
namespace Log {
class LogcatBackend : public Backend {
public:
static const char* Name() {
return "Logcat";
}
const char* GetName() const override {
return Name();
}
void Write(const Entry& entry) override;
};
} // namespace Log

View File

@ -1,22 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "native_interface.h"
namespace CitraJNI {
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
return JNI_VERSION_1_6;
}
std::string GetJString(JNIEnv* env, jstring jstr) {
std::string result = "";
if (!jstr)
return result;
const char* s = env->GetStringUTFChars(jstr, nullptr);
result = s;
env->ReleaseStringUTFChars(jstr, s);
return result;
}
} // namespace CitraJNI

View File

@ -1,16 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <string>
#include <jni.h>
namespace CitraJNI {
extern "C" {
jint JNI_OnLoad(JavaVM* vm, void* reserved);
}
std::string GetJString(JNIEnv* env, jstring jstr);
} // namespace CitraJNI

View File

@ -1,31 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/common_paths.h"
#include "common/file_util.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
#include "core/settings.h"
#include "logging/logcat_backend.h"
#include "native_interface.h"
namespace MainActivity {
extern "C" {
JNICALL void Java_org_citra_1emu_citra_ui_main_MainActivity_initUserPath(JNIEnv* env, jclass type,
jstring path) {
FileUtil::SetUserPath(CitraJNI::GetJString(env, path) + '/');
}
JNICALL void Java_org_citra_1emu_citra_ui_main_MainActivity_initLogging(JNIEnv* env, jclass type) {
Log::Filter log_filter(Log::Level::Debug);
log_filter.ParseFilterString(Settings::values.log_filter);
Log::SetGlobalFilter(log_filter);
const std::string& log_dir = FileUtil::GetUserPath(FileUtil::UserPath::LogDir);
FileUtil::CreateFullPath(log_dir);
Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE));
Log::AddBackend(std::make_unique<Log::LogcatBackend>());
}
};
}; // namespace MainActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,23 @@
package org.citra.citra_android;
import android.app.Application;
import org.citra.citra_android.model.GameDatabase;
import org.citra.citra_android.services.DirectoryInitializationService;
import org.citra.citra_android.utils.PermissionsHandler;
public class DolphinApplication extends Application
{
public static GameDatabase databaseHelper;
@Override
public void onCreate()
{
super.onCreate();
if (PermissionsHandler.hasWriteAccess(getApplicationContext()))
DirectoryInitializationService.startService(getApplicationContext());
databaseHelper = new GameDatabase(this);
}
}

View File

@ -0,0 +1,428 @@
/*
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_android;
import android.app.AlertDialog;
import android.view.Surface;
import org.citra.citra_android.activities.EmulationActivity;
import org.citra.citra_android.utils.Log;
import java.lang.ref.WeakReference;
/**
* Class which contains methods that interact
* with the native side of the Dolphin code.
*/
public final class NativeLibrary
{
public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null);
/**
* 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;
}
/**
* Button states
*/
public static final class ButtonState
{
public static final int RELEASED = 0;
public static final int PRESSED = 1;
}
private NativeLibrary()
{
// Disallows instantiation.
}
/**
* Default touchscreen device
*/
public static final String TouchScreenDevice = "Touchscreen";
/**
* 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.
*/
public static native void 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 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);
/**
* Gets a value from a key in the given ini-based config file.
*
* @param configFile The ini-based config file to get the value from.
* @param Section The section key that the actual key is in.
* @param Key The key to get the value from.
* @param Default The value to return in the event the given key doesn't exist.
* @return the value stored at the key, or a default value if it doesn't exist.
*/
public static native String GetConfig(String configFile, String Section, String Key,
String Default);
/**
* Sets a value to a key in the given ini config file.
*
* @param configFile The ini-based config file to add the value to.
* @param Section The section key for the ini key
* @param Key The actual ini key to set.
* @param Value The string to set the ini key to.
*/
public static native void SetConfig(String configFile, String Section, String Key, String Value);
/**
* Gets the embedded banner within the given ISO/ROM.
*
* @param filename the file path to the ISO/ROM.
* @return an integer array containing the color data for the banner.
*/
public static native int[] GetBanner(String filename);
/**
* Gets the embedded title of the given ISO/ROM.
*
* @param filename The file path to the ISO/ROM.
* @return the embedded title of the ISO/ROM.
*/
public static native String GetTitle(String filename);
public static native String GetDescription(String filename);
public static native String GetGameId(String filename);
public static native int GetCountry(String filename);
public static native String GetCompany(String filename);
public static native long GetFilesize(String filename);
public static native int GetPlatform(String filename);
/**
* Gets the Dolphin version string.
*
* @return the Dolphin version string.
*/
public static native String GetVersionString();
public static native String GetGitRevision();
/**
* Saves a screen capture of the game
*/
public static native void SaveScreenShot();
/**
* Saves a game state to the slot number.
*
* @param slot The slot location to save state to.
* @param wait If false, returns as early as possible.
* If true, returns once the savestate has been written to disk.
*/
public static native void SaveState(int slot, boolean wait);
/**
* Saves a game state to the specified path.
*
* @param path The path to save state to.
* @param wait If false, returns as early as possible.
* If true, returns once the savestate has been written to disk.
*/
public static native void SaveStateAs(String path, boolean wait);
/**
* Loads a game state from the slot number.
*
* @param slot The slot location to load state from.
*/
public static native void LoadState(int slot);
/**
* Loads a game state from the specified path.
*
* @param path The path to load state from.
*/
public static native void LoadStateAs(String path);
/**
* Sets the current working user directory
* If not set, it auto-detects a location
*/
public static native void SetUserDirectory(String directory);
/**
* Returns the current working user directory
*/
public static native String GetUserDirectory();
// Create the config.ini file.
public static native void CreateConfigFile();
public static native int DefaultCPUCore();
/**
* Begins emulation.
*/
public static native void Run(String path);
/**
* Begins emulation from the specified savestate.
*/
public static native void Run(String path, String savestatePath, boolean deleteSavestate);
public static native void ChangeDisc(String path);
// Surface Handling
public static native void SurfaceChanged(Surface surf);
public static native void SurfaceDestroyed();
/**
* 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();
/**
* Enables or disables CPU block profiling
*
* @param enable
*/
public static native void SetProfiling(boolean enable);
/**
* Writes out the block profile results
*/
public static native void WriteProfileResults();
/**
* Native EGL functions not exposed by Java bindings
**/
public static native void eglBindAPI(int api);
/**
* Provides a way to refresh the connections on Wiimotes
*/
public static native void RefreshWiimotes();
/**
* Returns the performance stats for the current game
**/
public static native double[] GetPerfStats();
/**
* The methods C++ uses to find references to Java classes and methods
* are really expensive. Rather than calling them every time we want to
* run them, do it once when we load the native library.
*/
private static native void CacheClassesAndMethods();
/**
* Switches the screen layout.
*/
public static native void SwitchScreenLayout();
/**
* Swaps the top and bottom screens.
*/
public static native void SwapScreens();
static
{
try
{
System.loadLibrary("main");
}
catch (UnsatisfiedLinkError ex)
{
Log.error("[NativeLibrary] " + ex.toString());
}
CacheClassesAndMethods();
}
private static boolean alertResult = false;
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();
AlertDialog.Builder builder = new AlertDialog.Builder(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("OK", (dialog, whichButton) ->
{
dialog.dismiss();
synchronized (lock)
{
lock.notify();
}
});
}
else
{
alertResult = false;
builder
.setPositiveButton("Yes", (dialog, whichButton) ->
{
alertResult = true;
dialog.dismiss();
synchronized (lock)
{
lock.notify();
}
})
.setNegativeButton("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 setEmulationActivity(EmulationActivity emulationActivity)
{
Log.verbose("[NativeLibrary] Registering EmulationActivity.");
sEmulationActivity = new WeakReference<>(emulationActivity);
}
public static void clearEmulationActivity()
{
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.");
sEmulationActivity.clear();
}
}

View File

@ -0,0 +1,29 @@
package org.citra.citra_android.activities;
import android.os.Environment;
import android.support.annotation.Nullable;
import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
import com.nononsenseapps.filepicker.FilePickerActivity;
import org.citra.citra_android.fragments.CustomFilePickerFragment;
import java.io.File;
public class CustomFilePickerActivity extends FilePickerActivity
{
@Override
protected AbstractFilePickerFragment<File> getFragment(
@Nullable final String startPath, final int mode, final boolean allowMultiple,
final boolean allowCreateDir, final boolean allowExistingFile,
final boolean singleClick)
{
AbstractFilePickerFragment<File> fragment = new CustomFilePickerFragment();
// startPath is allowed to be null. In that case, default folder should be SD-card and not "/"
fragment.setArgs(
startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(),
mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick);
return fragment;
}
}

View File

@ -0,0 +1,649 @@
package org.citra.citra_android.activities;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.IntDef;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.SparseIntArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.R;
import org.citra.citra_android.fragments.EmulationFragment;
import org.citra.citra_android.fragments.MenuFragment;
import org.citra.citra_android.ui.main.MainPresenter;
import org.citra.citra_android.utils.Animations;
import org.citra.citra_android.utils.ControllerMappingHelper;
import java.lang.annotation.Retention;
import java.util.List;
import static android.view.MotionEvent.AXIS_RZ;
import static android.view.MotionEvent.AXIS_X;
import static android.view.MotionEvent.AXIS_Y;
import static android.view.MotionEvent.AXIS_Z;
import static java.lang.annotation.RetentionPolicy.SOURCE;
public final class EmulationActivity extends AppCompatActivity
{
private static final String BACKSTACK_NAME_MENU = "menu";
private static final String BACKSTACK_NAME_SUBMENU = "submenu";
public static final int REQUEST_CHANGE_DISC = 1;
private View mDecorView;
private ImageView mImageView;
private EmulationFragment mEmulationFragment;
private SharedPreferences mPreferences;
private ControllerMappingHelper mControllerMappingHelper;
// So that MainActivity knows which view to invalidate before the return animation.
private int mPosition;
private boolean mDeviceHasTouchScreen;
private boolean mMenuVisible;
private boolean mBackPressedOnce;
private boolean activityRecreated;
private String mScreenPath;
private String mSelectedTitle;
private String mPath;
public static final String EXTRA_SELECTED_GAME = "SelectedGame";
public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
public static final String EXTRA_SCREEN_PATH = "ScreenPath";
public static final String EXTRA_GRID_POSITION = "GridPosition";
@Retention(SOURCE)
@IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE,
MENU_ACTION_EXIT, MENU_ACTION_TOGGLE_PREF_STATS, MENU_ACTION_SWITCH_SCREEN_LAYOUT,
MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY})
public @interface MenuAction
{
}
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_TOGGLE_PREF_STATS = 4;
public static final int MENU_ACTION_SWITCH_SCREEN_LAYOUT = 5;
public static final int MENU_ACTION_SWAP_SCREENS = 6;
public static final int MENU_ACTION_RESET_OVERLAY = 7;
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
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_toggle_perf_stats,
EmulationActivity.MENU_ACTION_TOGGLE_PREF_STATS);
buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT);
buttonsActionsMap.append(R.id.menu_emulation_switch_screen_layout,
EmulationActivity.MENU_ACTION_SWITCH_SCREEN_LAYOUT);
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);
}
public static void launch(FragmentActivity activity, String path, String title,
String screenshotPath, int position, View sharedView)
{
Intent launcher = new Intent(activity, EmulationActivity.class);
launcher.putExtra(EXTRA_SELECTED_GAME, path);
launcher.putExtra(EXTRA_SELECTED_TITLE, title);
launcher.putExtra(EXTRA_SCREEN_PATH, screenshotPath);
launcher.putExtra(EXTRA_GRID_POSITION, position);
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
sharedView,
"image_game_screenshot");
// I believe this warning is a bug. Activities are FragmentActivity from the support lib
//noinspection RestrictedApi
activity.startActivityForResult(launcher, MainPresenter.REQUEST_EMULATE_GAME,
options.toBundle());
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
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);
mScreenPath = gameToEmulate.getStringExtra(EXTRA_SCREEN_PATH);
mPosition = gameToEmulate.getIntExtra(EXTRA_GRID_POSITION, -1);
activityRecreated = false;
}
else
{
activityRecreated = true;
restoreState(savedInstanceState);
}
mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen");
mControllerMappingHelper = new ControllerMappingHelper();
int themeId;
if (mDeviceHasTouchScreen)
{
themeId = R.style.CitraEmulation;
// Get a handle to the Window containing the UI.
mDecorView = getWindow().getDecorView();
mDecorView.setOnSystemUiVisibilityChangeListener(visibility ->
{
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0)
{
// Go back to immersive fullscreen mode in 3s
Handler handler = new Handler(getMainLooper());
handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */);
}
});
// Set these options now so that the SurfaceView the game renders into is the right size.
enableFullscreenImmersive();
}
else
{
themeId = R.style.CitraEmulationTv;
}
setTheme(themeId);
setContentView(R.layout.activity_emulation);
mImageView = findViewById(R.id.image_screenshot);
// 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();
}
if (savedInstanceState == null)
{
// Picasso will take a while to load these big-ass screenshots. So don't run
// the animation until we say so.
postponeEnterTransition();
Picasso.with(this)
.load(mScreenPath)
.noFade()
.noPlaceholder()
.into(mImageView, new Callback()
{
@Override
public void onSuccess()
{
supportStartPostponedEnterTransition();
}
@Override
public void onError()
{
// Still have to do this, or else the app will crash.
supportStartPostponedEnterTransition();
}
});
Animations.fadeViewOut(mImageView)
.setStartDelay(2000)
.withEndAction(() -> mImageView.setVisibility(View.GONE));
}
else
{
mImageView.setVisibility(View.GONE);
}
if (mDeviceHasTouchScreen)
{
setTitle(mSelectedTitle);
}
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
outState.putString(EXTRA_SELECTED_GAME, mPath);
outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle);
outState.putString(EXTRA_SCREEN_PATH, mScreenPath);
outState.putInt(EXTRA_GRID_POSITION, mPosition);
super.onSaveInstanceState(outState);
}
protected void restoreState(Bundle savedInstanceState)
{
mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
mScreenPath = savedInstanceState.getString(EXTRA_SCREEN_PATH);
mPosition = savedInstanceState.getInt(EXTRA_GRID_POSITION);
}
@Override
public void onBackPressed()
{
if (!mDeviceHasTouchScreen)
{
boolean popResult = getSupportFragmentManager().popBackStackImmediate(
BACKSTACK_NAME_SUBMENU, FragmentManager.POP_BACK_STACK_INCLUSIVE);
if (!popResult)
{
toggleMenu();
}
}
else
{
if (mBackPressedOnce) {
mEmulationFragment.stopEmulation();
exitWithAnimation();
}
else
{
mBackPressedOnce = true;
Toast.makeText(this, "Press back again to exit", Toast.LENGTH_SHORT).show();
}
Handler mHandler = new Handler();
mHandler.postDelayed(new Runnable()
{
@Override public void run()
{
mBackPressedOnce = false;
mHandler.removeCallbacks(this);
}
}, 2000);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result)
{
}
private void enableFullscreenImmersive()
{
// It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
mDecorView.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);
}
private void toggleMenu()
{
boolean result = getSupportFragmentManager().popBackStackImmediate(
BACKSTACK_NAME_MENU, FragmentManager.POP_BACK_STACK_INCLUSIVE);
mMenuVisible = false;
if (!result)
{
// Removing the menu failed, so that means it wasn't visible. Add it.
Fragment fragment = MenuFragment.newInstance(mSelectedTitle);
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(
R.animator.menu_slide_in_from_left,
R.animator.menu_slide_out_to_left,
R.animator.menu_slide_in_from_left,
R.animator.menu_slide_out_to_left)
.add(R.id.frame_menu, fragment)
.addToBackStack(BACKSTACK_NAME_MENU)
.commit();
mMenuVisible = true;
}
}
public void exitWithAnimation()
{
runOnUiThread(() ->
{
Picasso.with(EmulationActivity.this)
.invalidate(mScreenPath);
Picasso.with(EmulationActivity.this)
.load(mScreenPath)
.noFade()
.noPlaceholder()
.into(mImageView, new Callback()
{
@Override
public void onSuccess()
{
showScreenshot();
}
@Override
public void onError()
{
finish();
}
});
});
}
private void showScreenshot()
{
Animations.fadeViewIn(mImageView)
.withEndAction(afterShowingScreenshot);
}
private Runnable afterShowingScreenshot = new Runnable()
{
@Override
public void run()
{
setResult(mPosition);
supportFinishAfterTransition();
}
};
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_emulation, menu);
return true;
}
@SuppressWarnings("WrongConstant")
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
int action = buttonsActionsMap.get(item.getItemId(), -1);
if (action >= 0)
{
handleMenuAction(action);
}
return true;
}
public void handleMenuAction(@MenuAction int menuAction)
{
switch (menuAction)
{
// 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();
return;
// Adjust the scale of the overlay controls.
case MENU_ACTION_ADJUST_SCALE:
adjustScale();
return;
// Toggle the visibility of the Performance stats TextView
case MENU_ACTION_TOGGLE_PREF_STATS:
mEmulationFragment.togglePerfStatsVisibility();
return;
// Switch the layout of the screens
case MENU_ACTION_SWITCH_SCREEN_LAYOUT:
NativeLibrary.SwitchScreenLayout();
return;
// Swap the top and bottom screen locations
case MENU_ACTION_SWAP_SCREENS:
NativeLibrary.SwapScreens();
return;
// Reset overlay placement
case MENU_ACTION_RESET_OVERLAY:
resetOverlay();
break;
case MENU_ACTION_EXIT:
toggleMenu(); // Hide the menu (it will be showing since we just clicked it)
mEmulationFragment.stopEmulation();
exitWithAnimation();
return;
}
}
private void editControlsPlacement()
{
if (mEmulationFragment.isConfiguringControls())
{
mEmulationFragment.stopConfiguringControls();
}
else
{
mEmulationFragment.startConfiguringControls();
}
}
// Gets button presses
@Override
public boolean dispatchKeyEvent(KeyEvent event)
{
if (mMenuVisible)
{
return super.dispatchKeyEvent(event);
}
int action;
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();
return NativeLibrary.onGamePadEvent(input.getDescriptor(), event.getKeyCode(), action);
}
private void toggleControls()
{
final SharedPreferences.Editor editor = mPreferences.edit();
boolean[] enabledButtons = new boolean[14];
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.emulation_toggle_controls);
for (int i = 0; i < enabledButtons.length; i++)
{
enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, true);
}
builder.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons,
(dialog, indexSelected, isChecked) -> editor
.putBoolean("buttonToggle" + indexSelected, isChecked));
builder.setNeutralButton(getString(R.string.emulation_toggle_all),
(dialogInterface, i) -> mEmulationFragment.toggleInputOverlayVisibility());
builder.setPositiveButton(getString(R.string.ok), (dialogInterface, i) ->
{
editor.apply();
mEmulationFragment.refreshInputOverlay();
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
private void adjustScale()
{
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.dialog_seekbar, null);
final SeekBar seekbar = view.findViewById(R.id.seekbar);
final TextView value = view.findViewById(R.id.text_value);
final TextView units = view.findViewById(R.id.text_units);
seekbar.setMax(150);
seekbar.setProgress(mPreferences.getInt("controlScale", 50));
seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener()
{
public void onStartTrackingTouch(SeekBar seekBar)
{
// Do nothing
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
value.setText(String.valueOf(progress + 50));
}
public void onStopTrackingTouch(SeekBar seekBar)
{
// Do nothing
}
});
value.setText(String.valueOf(seekbar.getProgress() + 50));
units.setText("%");
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.emulation_control_scale);
builder.setView(view);
builder.setPositiveButton(getString(R.string.ok), (dialogInterface, i) ->
{
SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt("controlScale", seekbar.getProgress());
editor.apply();
mEmulationFragment.refreshInputOverlay();
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
private void resetOverlay()
{
new AlertDialog.Builder(this)
.setTitle(getString(R.string.emulation_touch_overlay_reset))
.setPositiveButton(R.string.yes, (dialogInterface, i) ->
{
mEmulationFragment.resetInputOverlay();
})
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
{
})
.create()
.show();
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event)
{
if (mMenuVisible)
{
return false;
}
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[] axisValues = {0.0f, 0.0f};
for (InputDevice.MotionRange range : motions)
{
boolean consumed = false;
int axis = range.getAxis();
float origValue = event.getAxisValue(axis);
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
if (axis == AXIS_X || axis == AXIS_Z)
{
axisValues[0] = value;
}
else if (axis == AXIS_Y || axis == AXIS_RZ)
{
axisValues[1] = value;
}
// If the input is still in the "flat" area, that means it's really zero.
// This is used to compensate for imprecision in joysticks.
if (Math.abs(axisValues[0]) > range.getFlat() || Math.abs(axisValues[1]) > range.getFlat())
{
consumed = NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), axis, axisValues[0], axisValues[1]);
}
else
{
consumed = NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), axis, 0.0f, 0.0f);
}
return NativeLibrary.onGamePadAxisEvent(input.getDescriptor(),axis, value) || consumed;
}
return false;
}
public boolean isActivityRecreated()
{
return activityRecreated;
}
}

View File

@ -0,0 +1,251 @@
package org.citra.citra_android.adapters;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.support.v4.app.FragmentActivity;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.citra.citra_android.R;
import org.citra.citra_android.activities.EmulationActivity;
import org.citra.citra_android.model.GameDatabase;
import org.citra.citra_android.utils.Log;
import org.citra.citra_android.utils.PicassoUtils;
import org.citra.citra_android.viewholders.GameViewHolder;
/**
* 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> implements
View.OnClickListener
{
private Cursor mCursor;
private GameDataSetObserver mObserver;
private boolean mDatasetValid;
/**
* 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);
// 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.
*/
@Override
public void onBindViewHolder(GameViewHolder holder, int position)
{
if (mDatasetValid)
{
if (mCursor.moveToPosition(position))
{
String screenPath = mCursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH);
PicassoUtils.loadGameBanner(holder.imageScreenshot, screenPath,
mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE));
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
// 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.country = mCursor.getInt(GameDatabase.GAME_COLUMN_COUNTRY);
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY);
holder.screenshotPath = mCursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH);
}
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 card representing the game the user wants to play.
*/
@Override
public void onClick(View view)
{
GameViewHolder holder = (GameViewHolder) view.getTag();
EmulationActivity.launch((FragmentActivity) view.getContext(),
holder.path,
holder.title,
holder.screenshotPath,
holder.getAdapterPosition(),
holder.imageScreenshot);
}
public static class SpacesItemDecoration extends RecyclerView.ItemDecoration
{
private int space;
public SpacesItemDecoration(int space)
{
this.space = space;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state)
{
outRect.left = space;
outRect.right = space;
outRect.bottom = space;
outRect.top = space;
}
}
private final class GameDataSetObserver extends DataSetObserver
{
@Override
public void onChanged()
{
super.onChanged();
mDatasetValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated()
{
super.onInvalidated();
mDatasetValid = false;
notifyDataSetChanged();
}
}
}

View File

@ -0,0 +1,73 @@
package org.citra.citra_android.adapters;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.support.v4.content.ContextCompat;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.citra.citra_android.R;
import org.citra.citra_android.model.Game;
import org.citra.citra_android.utils.PicassoUtils;
import org.citra.citra_android.viewholders.TvGameViewHolder;
/**
* The Leanback library / docs call this a Presenter, but it works very
* similarly to a RecyclerView.Adapter.
*/
public final class GameRowPresenter extends Presenter
{
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent)
{
// Create a new view.
ImageCardView gameCard = new ImageCardView(parent.getContext());
gameCard.setMainImageAdjustViewBounds(true);
gameCard.setMainImageDimensions(48, 48);
gameCard.setMainImageScaleType(ImageView.ScaleType.CENTER_CROP);
gameCard.setFocusable(true);
gameCard.setFocusableInTouchMode(true);
// Use that view to create a ViewHolder.
return new TvGameViewHolder(gameCard);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object item)
{
TvGameViewHolder holder = (TvGameViewHolder) viewHolder;
Game game = (Game) item;
String screenPath = game.getScreenshotPath();
holder.imageScreenshot.setImageDrawable(null);
PicassoUtils.loadGameBanner(holder.imageScreenshot, screenPath, game.getPath());
holder.cardParent.setTitleText(game.getTitle());
holder.cardParent.setContentText(game.getCompany());
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
holder.gameId = game.getGameId();
holder.path = game.getPath();
holder.title = game.getTitle();
holder.description = game.getDescription();
holder.country = game.getCountry();
holder.company = game.getCompany();
holder.screenshotPath = game.getScreenshotPath();
// Set the platform-dependent background color of the card
Context context = holder.cardParent.getContext();
Drawable background = ContextCompat.getDrawable(context, R.drawable.tv_card_background_gamecube);
holder.cardParent.setInfoAreaBackground(background);
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder)
{
// no op
}
}

View File

@ -0,0 +1,46 @@
package org.citra.citra_android.adapters;
import android.content.res.Resources;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.view.ViewGroup;
import org.citra.citra_android.model.TvSettingsItem;
import org.citra.citra_android.viewholders.TvSettingsViewHolder;
public final class SettingsRowPresenter extends Presenter
{
public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent)
{
// Create a new view.
ImageCardView settingsCard = new ImageCardView(parent.getContext());
settingsCard.setMainImageAdjustViewBounds(true);
settingsCard.setMainImageDimensions(192, 160);
settingsCard.setFocusable(true);
settingsCard.setFocusableInTouchMode(true);
// Use that view to create a ViewHolder.
return new TvSettingsViewHolder(settingsCard);
}
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item)
{
TvSettingsViewHolder holder = (TvSettingsViewHolder) viewHolder;
TvSettingsItem settingsItem = (TvSettingsItem) item;
Resources resources = holder.cardParent.getResources();
holder.itemId = settingsItem.getItemId();
holder.cardParent.setTitleText(resources.getString(settingsItem.getLabelId()));
holder.cardParent.setMainImage(resources.getDrawable(settingsItem.getIconId(), null));
}
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder)
{
// no op
}
}

View File

@ -0,0 +1,97 @@
package org.citra.citra_android.dialogs;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.DialogFragment;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.picasso.Picasso;
import org.citra.citra_android.R;
import org.citra.citra_android.activities.EmulationActivity;
import de.hdodenhof.circleimageview.CircleImageView;
public final class GameDetailsDialog extends DialogFragment
{
private static final String ARG_GAME_TITLE = "game_title";
private static final String ARG_GAME_DESCRIPTION = "game_description";
private static final String ARG_GAME_COUNTRY = "game_country";
private static final String ARG_GAME_DATE = "game_date";
private static final String ARG_GAME_PATH = "game_path";
private static final String ARG_GAME_SCREENSHOT_PATH = "game_screenshot_path";
// TODO Add all of this to the Loader in GameActivity.java
public static GameDetailsDialog newInstance(String title, String description, int country,
String company, String path, String screenshotPath)
{
GameDetailsDialog fragment = new GameDetailsDialog();
Bundle arguments = new Bundle();
arguments.putString(ARG_GAME_TITLE, title);
arguments.putString(ARG_GAME_DESCRIPTION, description);
arguments.putInt(ARG_GAME_COUNTRY, country);
arguments.putString(ARG_GAME_DATE, company);
arguments.putString(ARG_GAME_PATH, path);
arguments.putString(ARG_GAME_SCREENSHOT_PATH, screenshotPath);
fragment.setArguments(arguments);
return fragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
ViewGroup contents = (ViewGroup) getActivity().getLayoutInflater()
.inflate(R.layout.dialog_game_details, null);
final ImageView imageGameScreen = contents.findViewById(R.id.image_game_screen);
CircleImageView circleBanner = contents.findViewById(R.id.circle_banner);
TextView textTitle = contents.findViewById(R.id.text_game_title);
TextView textDescription = contents.findViewById(R.id.text_company);
TextView textCountry = contents.findViewById(R.id.text_country);
TextView textDate = contents.findViewById(R.id.text_date);
FloatingActionButton buttonLaunch = contents.findViewById(R.id.button_launch);
int countryIndex = getArguments().getInt(ARG_GAME_COUNTRY);
String country = getResources().getStringArray(R.array.countryNames)[countryIndex];
textTitle.setText(getArguments().getString(ARG_GAME_TITLE));
textDescription.setText(getArguments().getString(ARG_GAME_DESCRIPTION));
textCountry.setText(country);
textDate.setText(getArguments().getString(ARG_GAME_DATE));
buttonLaunch.setOnClickListener(view ->
{
// Start the emulation activity and send the path of the clicked ROM to it.
EmulationActivity.launch(getActivity(),
getArguments().getString(ARG_GAME_PATH),
getArguments().getString(ARG_GAME_TITLE),
getArguments().getString(ARG_GAME_SCREENSHOT_PATH),
-1,
imageGameScreen);
});
// Fill in the view contents.
Picasso.with(imageGameScreen.getContext())
.load(getArguments().getString(ARG_GAME_SCREENSHOT_PATH))
.fit()
.centerCrop()
.noFade()
.noPlaceholder()
.into(imageGameScreen);
circleBanner.setImageResource(R.drawable.no_banner);
builder.setView(contents);
return builder.create();
}
}

View File

@ -0,0 +1,174 @@
package org.citra.citra_android.dialogs;
import android.app.AlertDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.citra.citra_android.model.settings.view.InputBindingSetting;
import org.citra.citra_android.utils.ControllerMappingHelper;
import org.citra.citra_android.utils.Log;
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 ControllerMappingHelper mControllerMappingHelper;
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;
this.mControllerMappingHelper = new ControllerMappingHelper();
}
public boolean onKeyEvent(int keyCode, KeyEvent event)
{
Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
switch (event.getAction())
{
case KeyEvent.ACTION_DOWN:
if (!mControllerMappingHelper.shouldKeyBeIgnored(event.getDevice(), keyCode))
{
saveKeyInput(event);
}
// Even if we ignore the key, we still consume it. Thus return true regardless.
return true;
default:
return false;
}
}
@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(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();
int numMovedAxis = 0;
float axisMoveValue = 0.0f;
InputDevice.MotionRange lastMovedRange = null;
char lastMovedDir = '?';
if (mWaitingForEvent)
{
// Get only the axis that seem to have moved (more than .5)
for (InputDevice.MotionRange range : motionRanges)
{
int axis = range.getAxis();
float origValue = event.getAxisValue(axis);
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
if (Math.abs(value) > 0.5f)
{
// It is common to have multiple axis 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 axis 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 ? '-' : '+';
}
}
}
// If only one axis moved, that's the winner.
if (numMovedAxis == 1)
{
mWaitingForEvent = false;
saveMotionInput(input, lastMovedRange, lastMovedDir);
}
}
return true;
}
/**
* Saves the provided key input setting both to the INI file (so native code can use it) and as
* an Android preference (so it persists correctly and is human-readable.)
*
* @param keyEvent KeyEvent of this key press.
*/
private void saveKeyInput(KeyEvent keyEvent)
{
InputDevice device = keyEvent.getDevice();
String bindStr = "Device '" + device.getDescriptor() + "'-Button " + keyEvent.getKeyCode();
String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
saveInput(bindStr, uiString);
}
/**
* Saves the provided motion input setting both to the INI file (so native code can use it) and as
* an Android preference (so it persists correctly and is human-readable.)
*
* @param device InputDevice from which the input event originated.
* @param motionRange MotionRange of the movement
* @param axisDir Either '-' or '+'
*/
private void saveMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
char axisDir)
{
String bindStr =
"Device '" + device.getDescriptor() + "'-Axis " + motionRange.getAxis() + axisDir;
String uiString = device.getName() + ": Axis " + motionRange.getAxis() + axisDir;
saveInput(bindStr, uiString);
}
/**
* Save the input string to settings and SharedPreferences, then dismiss this Dialog.
*/
private void saveInput(String bind, String ui)
{
setting.setValue(bind);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
SharedPreferences.Editor editor = preferences.edit();
editor.putString(setting.getKey(), ui);
editor.apply();
dismiss();
}
}

View File

@ -0,0 +1,22 @@
package org.citra.citra_android.fragments;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v4.content.FileProvider;
import com.nononsenseapps.filepicker.FilePickerFragment;
import java.io.File;
public class CustomFilePickerFragment extends FilePickerFragment
{
@NonNull
@Override
public Uri toUri(@NonNull final File file)
{
return FileProvider
.getUriForFile(getContext(),
getContext().getApplicationContext().getPackageName() + ".filesprovider",
file);
}
}

View File

@ -0,0 +1,491 @@
package org.citra.citra_android.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.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
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 org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.R;
import org.citra.citra_android.activities.EmulationActivity;
import org.citra.citra_android.overlay.InputOverlay;
import org.citra.citra_android.services.DirectoryInitializationService;
import org.citra.citra_android.services.DirectoryInitializationService.DirectoryInitializationState;
import org.citra.citra_android.utils.DirectoryStateReceiver;
import org.citra.citra_android.utils.Log;
public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback
{
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(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);
if (mInputOverlay != null)
{
// If the input overlay was previously disabled, then don't show it.
if (!mPreferences.getBoolean("showInputOverlay", true))
{
mInputOverlay.setVisibility(View.GONE);
}
}
Button doneButton = contents.findViewById(R.id.done_control_config);
if (doneButton != null)
{
doneButton.setOnClickListener(v -> stopConfiguringControls());
}
mPerfStats = contents.findViewById(R.id.perf_stats_text);
if (mPerfStats != null)
{
// If the overlay was previously disabled, then don't show it.
if (!mPreferences.getBoolean("showPerfStats", true))
{
mPerfStats.setVisibility(View.GONE);
}
else
{
updatePerfStats();
}
}
// The new Surface created here will get passed to the native code via onSurfaceChanged.
return contents;
}
@Override
public void onResume()
{
super.onResume();
if (DirectoryInitializationService.areDolphinDirectoriesReady())
{
mEmulationState.run(activity.isActivityRecreated());
}
else
{
setupDolphinDirectoriesThenStartEmulation();
}
}
@Override
public void onPause()
{
if (directoryStateReceiver != null)
{
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver);
directoryStateReceiver = null;
}
mEmulationState.pause();
super.onPause();
}
@Override
public void onDetach()
{
NativeLibrary.clearEmulationActivity();
super.onDetach();
}
private void setupDolphinDirectoriesThenStartEmulation()
{
IntentFilter statusIntentFilter = new IntentFilter(
DirectoryInitializationService.BROADCAST_ACTION);
directoryStateReceiver =
new DirectoryStateReceiver(directoryInitializationState ->
{
if (directoryInitializationState ==
DirectoryInitializationState.DOLPHIN_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);
DirectoryInitializationService.startService(getActivity());
}
public void toggleInputOverlayVisibility()
{
SharedPreferences.Editor editor = mPreferences.edit();
// If the overlay is currently set to INVISIBLE
if (!mPreferences.getBoolean("showInputOverlay", false))
{
// Set it to VISIBLE
mInputOverlay.setVisibility(View.VISIBLE);
editor.putBoolean("showInputOverlay", true);
}
else
{
// Set it to INVISIBLE
mInputOverlay.setVisibility(View.GONE);
editor.putBoolean("showInputOverlay", false);
}
editor.apply();
}
public void refreshInputOverlay()
{
mInputOverlay.refreshControls();
}
public void resetInputOverlay()
{
mInputOverlay.resetButtonPlacement();
}
public void togglePerfStatsVisibility()
{
SharedPreferences.Editor editor = mPreferences.edit();
// If the overlay is currently set to INVISIBLE
if (!mPreferences.getBoolean("showPerfStats", false))
{
updatePerfStats();
// Set it to VISIBLE
mPerfStats.setVisibility(View.VISIBLE);
editor.putBoolean("showPerfStats", true);
}
else
{
stopPerfStatsUpdates();
// Set it to INVISIBLE
mPerfStats.setVisibility(View.GONE);
editor.putBoolean("showPerfStats", false);
}
editor.apply();
}
private void updatePerfStats()
{
final int SYSTEM_FPS = 0;
final int FPS = 1;
final int FRAMETIME = 2;
final int SPEED = 3;
perfStatsUpdater = () ->
{
double perfStats[] = NativeLibrary.GetPerfStats();
mPerfStats
.setText(String.format("FPS: %.5s\nFrametime: %.7sms\nSpeed: %.4s%%", perfStats[FPS],
perfStats[FRAMETIME] * 1000.0, perfStats[SPEED] * 100.0));
perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000 /* 3s */);
};
perfStatsUpdateHandler.post(perfStatsUpdater);
}
private void stopPerfStatsUpdates()
{
if (perfStatsUpdater != null)
{
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater);
}
}
@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();
}
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 enum State
{
STOPPED, RUNNING, PAUSED
}
private final String mGamePath;
private Thread mEmulationThread;
private State state;
private Surface mSurface;
private boolean mRunWhenSurfaceIsValid;
EmulationState(String gamePath)
{
mGamePath = gamePath;
// Starting state is stopped.
state = State.STOPPED;
}
// Getters for the current state
public synchronized boolean isStopped()
{
return state == State.STOPPED;
}
public synchronized boolean isPaused()
{
return state == State.PAUSED;
}
public synchronized boolean isRunning()
{
return state == State.RUNNING;
}
// State changing methods
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.");
}
}
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)
{
mEmulationThread = new Thread(() ->
{
NativeLibrary.SurfaceChanged(mSurface);
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;
}
}
}

View File

@ -0,0 +1,78 @@
package org.citra.citra_android.fragments;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.citra.citra_android.R;
import org.citra.citra_android.activities.EmulationActivity;
public final class MenuFragment extends Fragment implements View.OnClickListener
{
private static final String KEY_TITLE = "title";
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
static
{
buttonsActionsMap.append(R.id.menu_emulation_toggle_perf_stats,
EmulationActivity.MENU_ACTION_TOGGLE_PREF_STATS);
buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT);
buttonsActionsMap.append(R.id.menu_emulation_switch_screen_layout,
EmulationActivity.MENU_ACTION_SWITCH_SCREEN_LAYOUT);
buttonsActionsMap.append(R.id.menu_emulation_swap_screens,
EmulationActivity.MENU_ACTION_SWAP_SCREENS);
}
public static MenuFragment newInstance(String title)
{
MenuFragment fragment = new MenuFragment();
Bundle arguments = new Bundle();
arguments.putSerializable(KEY_TITLE, title);
fragment.setArguments(arguments);
return fragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.fragment_ingame_menu, container, false);
LinearLayout options = rootView.findViewById(R.id.layout_options);
for (int childIndex = 0; childIndex < options.getChildCount(); childIndex++)
{
Button button = (Button) options.getChildAt(childIndex);
button.setOnClickListener(this);
}
TextView titleText = rootView.findViewById(R.id.text_game_title);
String title = getArguments().getString(KEY_TITLE);
if (title != null)
{
titleText.setText(title);
}
return rootView;
}
@SuppressWarnings("WrongConstant")
@Override
public void onClick(View button)
{
int action = buttonsActionsMap.get(button.getId());
if (action >= 0)
{
((EmulationActivity) getActivity()).handleMenuAction(action);
}
}
}

View File

@ -0,0 +1,96 @@
package org.citra.citra_android.model;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Environment;
public final class Game
{
private static final String PATH_SCREENSHOT_FOLDER =
"file://" + Environment.getExternalStorageDirectory().getPath() + "/citra-emu/ScreenShots/";
private String mTitle;
private String mDescription;
private String mPath;
private String mGameId;
private String mScreenshotPath;
private String mCompany;
private int mCountry;
public Game(String title, String description, int country, String path,
String gameId, String company, String screenshotPath)
{
mTitle = title;
mDescription = description;
mCountry = country;
mPath = path;
mGameId = gameId;
mCompany = company;
mScreenshotPath = screenshotPath;
}
public String getTitle()
{
return mTitle;
}
public String getDescription()
{
return mDescription;
}
public String getCompany()
{
return mCompany;
}
public int getCountry()
{
return mCountry;
}
public String getPath()
{
return mPath;
}
public String getGameId()
{
return mGameId;
}
public String getScreenshotPath()
{
return mScreenshotPath;
}
public static ContentValues asContentValues(String title, String description,
int country, String path, String gameId, String company)
{
ContentValues values = new ContentValues();
String screenPath = PATH_SCREENSHOT_FOLDER + gameId + "/" + gameId + "-1.png";
values.put(GameDatabase.KEY_GAME_TITLE, title);
values.put(GameDatabase.KEY_GAME_DESCRIPTION, description);
values.put(GameDatabase.KEY_GAME_COUNTRY, country);
values.put(GameDatabase.KEY_GAME_PATH, path);
values.put(GameDatabase.KEY_GAME_ID, gameId);
values.put(GameDatabase.KEY_GAME_COMPANY, company);
values.put(GameDatabase.KEY_GAME_SCREENSHOT_PATH, screenPath);
return values;
}
public static Game fromCursor(Cursor cursor)
{
return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE),
cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
cursor.getInt(GameDatabase.GAME_COLUMN_COUNTRY),
cursor.getString(GameDatabase.GAME_COLUMN_PATH),
cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
cursor.getString(GameDatabase.GAME_COLUMN_COMPANY),
cursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH));
}
}

View File

@ -0,0 +1,292 @@
package org.citra.citra_android.model;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.utils.Log;
import java.io.File;
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
{
private static final int DB_VERSION = 1;
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_COUNTRY = 4;
public static final int GAME_COLUMN_GAME_ID = 5;
public static final int GAME_COLUMN_COMPANY = 6;
public static final int GAME_COLUMN_SCREENSHOT_PATH = 7;
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_COUNTRY = "country";
public static final String KEY_GAME_ID = "game_id";
public static final String KEY_GAME_COMPANY = "company";
public static final String KEY_GAME_SCREENSHOT_PATH = "screenshot_path";
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 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_COUNTRY + TYPE_INTEGER + SEPARATOR
+ KEY_GAME_ID + TYPE_STRING + SEPARATOR
+ KEY_GAME_COMPANY + TYPE_STRING + SEPARATOR
+ KEY_GAME_SCREENSHOT_PATH + 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;
public GameDatabase(Context context)
{
// Superclass constructor builds a database or uses an existing one.
super(context, "games.db", null, DB_VERSION);
}
@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);
Log.verbose("[GameDatabase] Re-scanning library with new schema.");
scanLibrary(database);
}
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);
File game = new File(gamePath);
if (!game.exists())
{
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", ".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app"));
// 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);
File folder = new File(folderPath);
Log.info("[GameDatabase] Reading files from library folder: " + folderPath);
// Iterate through every file in the folder.
File[] children = folder.listFiles();
if (children != null)
{
for (File file : children)
{
if (!file.isHidden() && !file.isDirectory())
{
String filePath = file.getPath();
int extensionStart = filePath.lastIndexOf('.');
if (extensionStart > 0)
{
String fileExtension = filePath.substring(extensionStart);
// Check that the file has an extension we care about before trying to read out of it.
if (allowedExtensions.contains(fileExtension.toLowerCase()))
{
String name = NativeLibrary.GetTitle(filePath);
// If the game's title field is empty, use the filename.
if (name.isEmpty())
{
name = filePath.substring(filePath.lastIndexOf("/") + 1);
}
String gameId = NativeLibrary.GetGameId(filePath);
// If the game's ID field is empty, use the filename without extension.
if (gameId.isEmpty())
{
gameId = filePath.substring(filePath.lastIndexOf("/") + 1,
filePath.lastIndexOf("."));
}
ContentValues game = Game.asContentValues(name,
NativeLibrary.GetDescription(filePath).replace("\n", " "),
NativeLibrary.GetCountry(filePath),
filePath,
gameId,
NativeLibrary.GetCompany(filePath));
// 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));
}
}
}
}
}
}
// If the folder is empty because it no longer exists, remove it from the library.
else if (!folder.exists())
{
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))});
}
else
{
Log.error("[GameDatabase] Folder contains no games: " + folderPath);
}
}
fileCursor.close();
folderCursor.close();
database.close();
}
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);
}
}

View File

@ -0,0 +1,155 @@
package org.citra.citra_android.model;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.citra.citra_android.BuildConfig;
import org.citra.citra_android.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 AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider";
public static final Uri URI_FOLDER =
Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/");
public static final Uri URI_GAME =
Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_GAMES + "/");
public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_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();
long id = -1;
if (table != null)
{
if (table.equals(REFRESH_LIBRARY))
{
Log.info(
"[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents...");
mDbHelper.scanLibrary(database);
return uri;
}
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;
}
}

View File

@ -0,0 +1,30 @@
package org.citra.citra_android.model;
public final class TvSettingsItem
{
private final int mItemId;
private final int mIconId;
private final int mLabelId;
public TvSettingsItem(int itemId, int iconId, int labelId)
{
mItemId = itemId;
mIconId = iconId;
mLabelId = labelId;
}
public int getItemId()
{
return mItemId;
}
public int getIconId()
{
return mIconId;
}
public int getLabelId()
{
return mLabelId;
}
}

View File

@ -0,0 +1,28 @@
package org.citra.citra_android.model.settings;
public final class FloatSetting extends Setting
{
private float mValue;
public FloatSetting(String key, String section, int file, float value)
{
super(key, section, file);
mValue = value;
}
public float getValue()
{
return mValue;
}
public void setValue(float value)
{
mValue = value;
}
@Override
public String getValueAsString()
{
return Float.toString(mValue);
}
}

View File

@ -0,0 +1,28 @@
package org.citra.citra_android.model.settings;
public final class IntSetting extends Setting
{
private int mValue;
public IntSetting(String key, String section, int file, int value)
{
super(key, section, file);
mValue = value;
}
public int getValue()
{
return mValue;
}
public void setValue(int value)
{
mValue = value;
}
@Override
public String getValueAsString()
{
return Integer.toString(mValue);
}
}

View File

@ -0,0 +1,57 @@
package org.citra.citra_android.model.settings;
/**
* Abstraction for a setting item as read from / written to Dolphin'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;
private int mFile;
/**
* 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.
* @param file The ini file the Setting is stored in.
*/
public Setting(String key, String section, int file)
{
mKey = key;
mSection = section;
mFile = file;
}
/**
* @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 The ini file the Setting is stored in.
*/
public int getFile()
{
return mFile;
}
/**
* @return A representation of this Setting's backing value converted to a String (e.g. for serialization).
*/
public abstract String getValueAsString();
}

View File

@ -0,0 +1,55 @@
package org.citra.citra_android.model.settings;
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;
}
}

View File

@ -0,0 +1,28 @@
package org.citra.citra_android.model.settings;
public final class StringSetting extends Setting
{
private String mValue;
public StringSetting(String key, String section, int file, String value)
{
super(key, section, file);
mValue = value;
}
public String getValue()
{
return mValue;
}
public void setValue(String value)
{
mValue = value;
}
@Override
public String getValueAsString()
{
return mValue;
}
}

View File

@ -0,0 +1,56 @@
package org.citra.citra_android.model.settings.view;
import org.citra.citra_android.model.settings.IntSetting;
import org.citra.citra_android.model.settings.Setting;
public final class CheckBoxSetting extends SettingsItem
{
private boolean mDefaultValue;
public CheckBoxSetting(String key, String section, int file, int titleId, int descriptionId,
boolean defaultValue, Setting setting)
{
super(key, section, file, setting, titleId, descriptionId);
mDefaultValue = defaultValue;
}
public boolean isChecked()
{
if (getSetting() == null)
{
return mDefaultValue;
}
IntSetting setting = (IntSetting) getSetting();
return setting.getValue() == 1;
}
/**
* 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)
{
if (getSetting() == null)
{
IntSetting setting = new IntSetting(getKey(), getSection(), getFile(), 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;
}
}

View File

@ -0,0 +1,41 @@
package org.citra.citra_android.model.settings.view;
import org.citra.citra_android.model.settings.Setting;
import org.citra.citra_android.model.settings.StringSetting;
public final class DateTimeSetting extends SettingsItem
{
private String mDefaultValue;
public DateTimeSetting(String key, String section, int file, int titleId, int descriptionId,
String defaultValue, Setting setting)
{
super(key, section, file, 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)
{
StringSetting setting = new StringSetting(getKey(), getSection(), getFile(), datetime);
setSetting(setting);
return setting;
}
@Override
public int getType()
{
return TYPE_DATETIME_SETTING;
}
}

View File

@ -0,0 +1,17 @@
package org.citra.citra_android.model.settings.view;
import org.citra.citra_android.model.settings.Setting;
public final class HeaderSetting extends SettingsItem
{
public HeaderSetting(String key, Setting setting, int titleId, int descriptionId)
{
super(key, null, 0, setting, titleId, descriptionId);
}
@Override
public int getType()
{
return SettingsItem.TYPE_HEADER;
}
}

View File

@ -0,0 +1,52 @@
package org.citra.citra_android.model.settings.view;
import org.citra.citra_android.model.settings.Setting;
import org.citra.citra_android.model.settings.StringSetting;
public final class InputBindingSetting extends SettingsItem
{
public InputBindingSetting(String key, String section, int file, int titleId, Setting setting)
{
super(key, section, file, setting, titleId, 0);
}
public String getValue()
{
if (getSetting() == null)
{
return "";
}
StringSetting setting = (StringSetting) getSetting();
return setting.getValue();
}
/**
* Write a value to the backing string. If that string was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param bind The input that will be bound
* @return null if overwritten successfully; otherwise, a newly created StringSetting.
*/
public StringSetting setValue(String bind)
{
if (getSetting() == null)
{
StringSetting setting = new StringSetting(getKey(), getSection(), getFile(), bind);
setSetting(setting);
return setting;
}
else
{
StringSetting setting = (StringSetting) getSetting();
setting.setValue(bind);
return null;
}
}
@Override
public int getType()
{
return TYPE_INPUT_BINDING;
}
}

View File

@ -0,0 +1,115 @@
package org.citra.citra_android.model.settings.view;
import org.citra.citra_android.model.settings.Setting;
/**
* 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_DATETIME_SETTING = 6;
private String mKey;
private String mSection;
private int mFile;
private Setting mSetting;
private int mNameId;
private int mDescriptionId;
/**
* 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, int file, Setting setting, int nameId,
int descriptionId)
{
mKey = key;
mSection = section;
mFile = file;
mSetting = setting;
mNameId = nameId;
mDescriptionId = descriptionId;
}
/**
* @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 file the backing Setting is saved to.
*/
public int getFile()
{
return mFile;
}
/**
* @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;
}
/**
* Used by {@link org.citra.citra_android.ui.settings.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();
}

View File

@ -0,0 +1,73 @@
package org.citra.citra_android.model.settings.view;
import org.citra.citra_android.model.settings.IntSetting;
import org.citra.citra_android.model.settings.Setting;
public final class SingleChoiceSetting extends SettingsItem
{
private int mDefaultValue;
private int mChoicesId;
private int mValuesId;
public SingleChoiceSetting(String key, String section, int file, int titleId, int descriptionId,
int choicesId, int valuesId, int defaultValue, Setting setting)
{
super(key, section, file, 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(), getFile(), selection);
setSetting(setting);
return setting;
}
else
{
IntSetting setting = (IntSetting) getSetting();
setting.setValue(selection);
return null;
}
}
@Override
public int getType()
{
return TYPE_SINGLE_CHOICE;
}
}

View File

@ -0,0 +1,119 @@
package org.citra.citra_android.model.settings.view;
import org.citra.citra_android.model.settings.FloatSetting;
import org.citra.citra_android.model.settings.IntSetting;
import org.citra.citra_android.model.settings.Setting;
import org.citra.citra_android.utils.Log;
import org.citra.citra_android.utils.SettingsFile;
public final class SliderSetting extends SettingsItem
{
private int mMax;
private int mDefaultValue;
private String mUnits;
public SliderSetting(String key, String section, int file, int titleId, int descriptionId,
int max, String units, int defaultValue, Setting setting)
{
super(key, section, file, setting, titleId, descriptionId);
mMax = max;
mUnits = units;
mDefaultValue = defaultValue;
}
public int getMax()
{
return mMax;
}
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;
if (floatSetting.getKey().equals(SettingsFile.KEY_FRAME_LIMIT))
{
return Math.round(floatSetting.getValue() * 100);
}
else
{
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(), getFile(), 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(), getFile(), 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;
}
}

View File

@ -0,0 +1,25 @@
package org.citra.citra_android.model.settings.view;
import org.citra.citra_android.model.settings.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, 0, setting, titleId, descriptionId);
mMenuKey = menuKey;
}
public String getMenuKey()
{
return mMenuKey;
}
@Override
public int getType()
{
return TYPE_SUBMENU;
}
}

View File

@ -0,0 +1,907 @@
/**
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_android.overlay;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.Display;
import android.view.View.OnTouchListener;
import org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.NativeLibrary.ButtonState;
import org.citra.citra_android.NativeLibrary.ButtonType;
import org.citra.citra_android.R;
import java.util.HashSet;
import java.util.Set;
/**
* Draws the interactive input overlay on top of the
* {@link SurfaceView} that is rendering emulation.
*/
public final class InputOverlay extends SurfaceView implements OnTouchListener
{
private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<>();
private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>();
private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<>();
private boolean mIsInEditMode = false;
private InputOverlayDrawableButton mButtonBeingConfigured;
private InputOverlayDrawableDpad mDpadBeingConfigured;
private InputOverlayDrawableJoystick mJoystickBeingConfigured;
private SharedPreferences mPreferences;
/**
* Resizes a {@link Bitmap} by a given scale factor
*
* @param context The current {@link Context}
* @param bitmap The {@link Bitmap} to scale.
* @param scale The scale factor for the bitmap.
* @return The scaled {@link Bitmap}
*/
public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale)
{
// Determine the button size based on the smaller screen dimension.
// This makes sure the buttons are the same size in both portrait and landscape.
DisplayMetrics dm = context.getResources().getDisplayMetrics();
int minDimension = Math.min(dm.widthPixels, dm.heightPixels);
return Bitmap.createScaledBitmap(bitmap,
(int) (minDimension * scale),
(int) (minDimension * scale),
true);
}
/**
* Constructor
*
* @param context The current {@link Context}.
* @param attrs {@link AttributeSet} for parsing XML attributes.
*/
public InputOverlay(Context context, AttributeSet attrs)
{
super(context, attrs);
mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
if (!mPreferences.getBoolean("OverlayInit", false)) {
defaultOverlay();
}
// Load the controls.
refreshControls();
// Set the on touch listener.
setOnTouchListener(this);
// Force draw
setWillNotDraw(false);
// Request focus for the overlay so it has priority on presses.
requestFocus();
}
@Override
public void draw(Canvas canvas)
{
super.draw(canvas);
for (InputOverlayDrawableButton button : overlayButtons)
{
button.draw(canvas);
}
for (InputOverlayDrawableDpad dpad : overlayDpads)
{
dpad.draw(canvas);
}
for (InputOverlayDrawableJoystick joystick : overlayJoysticks)
{
joystick.draw(canvas);
}
}
@Override
public boolean onTouch(View v, MotionEvent event)
{
if (isInEditMode())
{
return onTouchWhileEditing(event);
}
int pointerIndex = event.getActionIndex();
if (mPreferences.getBoolean("isTouchEnabled", true))
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
NativeLibrary.onTouchEvent(event.getX(pointerIndex), event.getY(pointerIndex), true);
break;
case MotionEvent.ACTION_MOVE:
NativeLibrary.onTouchMoved(event.getX(), event.getY());
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// We dont really care where the touch has been released. We only care whether it has been
// released or not.
NativeLibrary.onTouchEvent(0, 0, false);
break;
}
}
for (InputOverlayDrawableButton button : overlayButtons)
{
// Determine the button state to apply based on the MotionEvent action flag.
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// If a pointer enters the bounds of a button, press that button.
if (button.getBounds()
.contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex)))
{
button.setPressedState(true);
button.setTrackId(event.getPointerId(pointerIndex));
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(),
ButtonState.PRESSED);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// If a pointer ends, release the button it was pressing.
if (button.getTrackId() == event.getPointerId(pointerIndex))
{
button.setPressedState(false);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(),
ButtonState.RELEASED);
}
break;
}
}
for (InputOverlayDrawableDpad dpad : overlayDpads)
{
// Determine the button state to apply based on the MotionEvent action flag.
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// If a pointer enters the bounds of a button, press that button.
if (dpad.getBounds()
.contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex)))
{
boolean up = false;
boolean down = false;
boolean left = false;
boolean right = false;
if (dpad.getBounds().top + (dpad.getHeight() / 3) > (int) event.getY(pointerIndex))
{
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0),
ButtonState.PRESSED);
up = true;
}
if (dpad.getBounds().bottom - (dpad.getHeight() / 3) < (int) event.getY(pointerIndex))
{
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1),
ButtonState.PRESSED);
down = true;
}
if (dpad.getBounds().left + (dpad.getWidth() / 3) > (int) event.getX(pointerIndex))
{
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2),
ButtonState.PRESSED);
left = true;
}
if (dpad.getBounds().right - (dpad.getWidth() / 3) < (int) event.getX(pointerIndex))
{
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3),
ButtonState.PRESSED);
right = true;
}
setDpadState(dpad, up, down, left, right);
dpad.setTrackId(event.getPointerId(pointerIndex));
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// If a pointer ends, release the buttons.
if (dpad.getTrackId() == event.getPointerId(pointerIndex))
{
for (int i = 0; i < 4; i++)
{
dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(i),
ButtonState.RELEASED);
}
}
break;
}
}
for (InputOverlayDrawableJoystick joystick : overlayJoysticks)
{
joystick.TrackEvent(event);
int axisID = joystick.getId();
float[] axises = joystick.getAxisValues();
NativeLibrary
.onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, axises[0], axises[1]);
}
invalidate();
return true;
}
public boolean onTouchWhileEditing(MotionEvent event)
{
int pointerIndex = event.getActionIndex();
int fingerPositionX = (int) event.getX(pointerIndex);
int fingerPositionY = (int) event.getY(pointerIndex);
// Maybe combine Button and Joystick as subclasses of the same parent?
// Or maybe create an interface like IMoveableHUDControl?
for (InputOverlayDrawableButton button : overlayButtons)
{
// Determine the button state to apply based on the MotionEvent action flag.
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// If no button is being moved now, remember the currently touched button to move.
if (mButtonBeingConfigured == null &&
button.getBounds().contains(fingerPositionX, fingerPositionY))
{
mButtonBeingConfigured = button;
mButtonBeingConfigured.onConfigureTouch(event);
}
break;
case MotionEvent.ACTION_MOVE:
if (mButtonBeingConfigured != null)
{
mButtonBeingConfigured.onConfigureTouch(event);
invalidate();
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (mButtonBeingConfigured == button)
{
// Persist button position by saving new place.
saveControlPosition(mButtonBeingConfigured.getId(),
mButtonBeingConfigured.getBounds().left,
mButtonBeingConfigured.getBounds().top);
mButtonBeingConfigured = null;
}
break;
}
}
for (InputOverlayDrawableDpad dpad : overlayDpads)
{
// Determine the button state to apply based on the MotionEvent action flag.
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// If no button is being moved now, remember the currently touched button to move.
if (mButtonBeingConfigured == null &&
dpad.getBounds().contains(fingerPositionX, fingerPositionY))
{
mDpadBeingConfigured = dpad;
mDpadBeingConfigured.onConfigureTouch(event);
}
break;
case MotionEvent.ACTION_MOVE:
if (mDpadBeingConfigured != null)
{
mDpadBeingConfigured.onConfigureTouch(event);
invalidate();
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (mDpadBeingConfigured == dpad)
{
// Persist button position by saving new place.
saveControlPosition(mDpadBeingConfigured.getId(0),
mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top);
mDpadBeingConfigured = null;
}
break;
}
}
for (InputOverlayDrawableJoystick joystick : overlayJoysticks)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
if (mJoystickBeingConfigured == null &&
joystick.getBounds().contains(fingerPositionX, fingerPositionY))
{
mJoystickBeingConfigured = joystick;
mJoystickBeingConfigured.onConfigureTouch(event);
}
break;
case MotionEvent.ACTION_MOVE:
if (mJoystickBeingConfigured != null)
{
mJoystickBeingConfigured.onConfigureTouch(event);
invalidate();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (mJoystickBeingConfigured != null)
{
saveControlPosition(mJoystickBeingConfigured.getId(),
mJoystickBeingConfigured.getBounds().left,
mJoystickBeingConfigured.getBounds().top);
mJoystickBeingConfigured = null;
}
break;
}
}
return true;
}
private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left,
boolean right)
{
if (up)
{
if (left)
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT);
else if (right)
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT);
else
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP);
}
else if (down)
{
if (left)
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT);
else if (right)
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT);
else
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN);
}
else if (left)
{
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT);
}
else if (right)
{
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT);
}
}
private void addOverlayControls(String orientation)
{
if (mPreferences.getBoolean("buttonToggle0", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a,
R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation));
}
if (mPreferences.getBoolean("buttonToggle1", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b,
R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation));
}
if (mPreferences.getBoolean("buttonToggle2", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x,
R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation));
}
if (mPreferences.getBoolean("buttonToggle3", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y,
R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation));
}
if (mPreferences.getBoolean("buttonToggle4", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l,
R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation));
}
if (mPreferences.getBoolean("buttonToggle5", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r,
R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation));
}
if (mPreferences.getBoolean("buttonToggle6", false))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl,
R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation));
}
if (mPreferences.getBoolean("buttonToggle7", false))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr,
R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation));
}
if (mPreferences.getBoolean("buttonToggle8", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start,
R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation));
}
if (mPreferences.getBoolean("buttonToggle9", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select,
R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation));
}
if (mPreferences.getBoolean("buttonToggle10", true))
{
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_home,
R.drawable.button_home_pressed, ButtonType.BUTTON_HOME, orientation));
}
if (mPreferences.getBoolean("buttonToggle11", false))
{
overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad,
R.drawable.dpad_pressed_one_direction,
R.drawable.dpad_pressed_two_directions,
ButtonType.DPAD_UP, ButtonType.DPAD_DOWN,
ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation));
}
if (mPreferences.getBoolean("buttonToggle12", true))
{
overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range,
R.drawable.stick_main, R.drawable.stick_main_pressed,
ButtonType.STICK_LEFT, orientation));
}
if (mPreferences.getBoolean("buttonToggle13", false))
{
overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range,
R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation));
}
}
public void refreshControls()
{
// Remove all the overlay buttons from the HashSet.
overlayButtons.removeAll(overlayButtons);
overlayDpads.removeAll(overlayDpads);
overlayJoysticks.removeAll(overlayJoysticks);
String orientation =
getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ?
"-Portrait" : "";
if (mPreferences.getBoolean("showInputOverlay", true))
{
// Add all the enabled overlay items back to the HashSet.
addOverlayControls(orientation);
}
invalidate();
}
private void saveControlPosition(int sharedPrefsId, int x, int y)
{
final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
SharedPreferences.Editor sPrefsEditor = sPrefs.edit();
sPrefsEditor.putFloat(sharedPrefsId + "-X", x);
sPrefsEditor.putFloat(sharedPrefsId + "-Y", y);
sPrefsEditor.apply();
}
/**
* Initializes an InputOverlayDrawableButton, given by resId, with all of the
* parameters set for it to be properly shown on the InputOverlay.
* <p>
* This works due to the way the X and Y coordinates are stored within
* the {@link SharedPreferences}.
* <p>
* In the input overlay configuration menu,
* once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
* the X and Y coordinates of the button at the END of its touch event
* (when you remove your finger/stylus from the touchscreen) are then stored
* within a SharedPreferences instance so that those values can be retrieved here.
* <p>
* This has a few benefits over the conventional way of storing the values
* (ie. within the Dolphin ini file).
* <ul>
* <li>No native calls</li>
* <li>Keeps Android-only values inside the Android environment</li>
* </ul>
* <p>
* Technically no modifications should need to be performed on the returned
* InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
* for Android to call the onDraw method.
*
* @param context The current {@link Context}.
* @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State).
* @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State).
* @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
* @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set.
*/
private static InputOverlayDrawableButton initializeOverlayButton(Context context,
int defaultResId, int pressedResId, int buttonId, String orientation)
{
// Resources handle for fetching the initial Drawable resource.
final Resources res = context.getResources();
// SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
// Decide scale based on button ID and user preference
float scale;
switch (buttonId)
{
case ButtonType.BUTTON_HOME:
case ButtonType.BUTTON_START:
case ButtonType.BUTTON_SELECT:
scale = 0.0625f;
break;
case ButtonType.TRIGGER_L:
case ButtonType.TRIGGER_R:
case ButtonType.BUTTON_ZL:
case ButtonType.BUTTON_ZR:
scale = 0.25f;
break;
default:
scale = 0.125f;
break;
}
scale *= (sPrefs.getInt("controlScale", 50) + 50);
scale /= 100;
// Initialize the InputOverlayDrawableButton.
final Bitmap defaultStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale);
final Bitmap pressedStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale);
final InputOverlayDrawableButton overlayDrawable =
new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId);
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
String xKey;
String yKey;
xKey = buttonId + orientation + "-X";
yKey = buttonId + orientation + "-Y";
int drawableX = (int) sPrefs.getFloat(xKey, 0f);
int drawableY = (int) sPrefs.getFloat(yKey, 0f);
int width = overlayDrawable.getWidth();
int height = overlayDrawable.getHeight();
// Now set the bounds for the InputOverlayDrawableButton.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height);
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY);
return overlayDrawable;
}
/**
* Initializes an {@link InputOverlayDrawableDpad}
*
* @param context The current {@link Context}.
* @param defaultResId The {@link Bitmap} resource ID of the default sate.
* @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction.
* @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions.
* @param buttonUp Identifier for the up button.
* @param buttonDown Identifier for the down button.
* @param buttonLeft Identifier for the left button.
* @param buttonRight Identifier for the right button.
* @return the initialized {@link InputOverlayDrawableDpad}
*/
private static InputOverlayDrawableDpad initializeOverlayDpad(Context context,
int defaultResId,
int pressedOneDirectionResId,
int pressedTwoDirectionsResId,
int buttonUp,
int buttonDown,
int buttonLeft,
int buttonRight,
String orientation)
{
// Resources handle for fetching the initial Drawable resource.
final Resources res = context.getResources();
// SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
// Decide scale based on button ID and user preference
float scale;
switch (buttonUp)
{
case ButtonType.DPAD_UP:
scale = 0.275f;
break;
default:
scale = 0.2125f;
break;
}
scale *= (sPrefs.getInt("controlScale", 50) + 50);
scale /= 100;
// Initialize the InputOverlayDrawableDpad.
final Bitmap defaultStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale);
final Bitmap pressedOneDirectionStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId),
scale);
final Bitmap pressedTwoDirectionsStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId),
scale);
final InputOverlayDrawableDpad overlayDrawable =
new InputOverlayDrawableDpad(res, defaultStateBitmap,
pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap,
buttonUp, buttonDown, buttonLeft, buttonRight);
// The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
// These were set in the input overlay configuration menu.
int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f);
int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f);
int width = overlayDrawable.getWidth();
int height = overlayDrawable.getHeight();
// Now set the bounds for the InputOverlayDrawableDpad.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height);
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY);
return overlayDrawable;
}
/**
* Initializes an {@link InputOverlayDrawableJoystick}
*
* @param context The current {@link Context}
* @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
* @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
* @param pressedResInner Resource ID for the pressed inner image of the joystick.
* @param joystick Identifier for which joystick this is.
* @return the initialized {@link InputOverlayDrawableJoystick}.
*/
private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context,
int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation)
{
// Resources handle for fetching the initial Drawable resource.
final Resources res = context.getResources();
// SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
// Decide scale based on user preference
float scale = 0.275f;
scale *= (sPrefs.getInt("controlScale", 50) + 50);
scale /= 100;
// Initialize the InputOverlayDrawableJoystick.
final Bitmap bitmapOuter =
resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale);
final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner);
final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner);
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f);
int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f);
// Decide inner scale based on joystick ID
float innerScale;
switch (joystick)
{
case ButtonType.STICK_C:
innerScale = 1.833f;
break;
default:
innerScale = 1.375f;
break;
}
// Now set the bounds for the InputOverlayDrawableJoystick.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
int outerSize = bitmapOuter.getWidth();
Rect outerRect = new Rect(drawableX, drawableY, drawableX + outerSize, drawableY + outerSize);
Rect innerRect = new Rect(0, 0, (int) (outerSize / innerScale), (int) (outerSize / innerScale));
// Send the drawableId to the joystick so it can be referenced when saving control position.
final InputOverlayDrawableJoystick overlayDrawable
= new InputOverlayDrawableJoystick(res, bitmapOuter,
bitmapInnerDefault, bitmapInnerPressed,
outerRect, innerRect, joystick);
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY);
return overlayDrawable;
}
public void setIsInEditMode(boolean isInEditMode)
{
mIsInEditMode = isInEditMode;
}
private void defaultOverlay()
{
if (!mPreferences.getBoolean("OverlayInit", false))
{
// It's possible that a user has created their overlay before this was added
// Only change the overlay if the 'A' button is not in the upper corner.
// GameCube
if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f)
{
defaultOverlayLandscape();
}
if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f)
{
defaultOverlayPortrait();
}
}
SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
sPrefsEditor.putBoolean("OverlayInit", true);
sPrefsEditor.apply();
}
public void resetButtonPlacement()
{
boolean isLandscape =
getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
if (isLandscape) {
defaultOverlayLandscape();
}
else {
defaultOverlayPortrait();
}
refreshControls();
}
private void defaultOverlayLandscape()
{
SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
// Get screen size
Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
float maxX = outMetrics.heightPixels;
float maxY = outMetrics.widthPixels;
// Height and width changes depending on orientation. Use the larger value for height.
if (maxY > maxX)
{
float tmp = maxX;
maxX = maxY;
maxY = tmp;
}
Resources res = getResources();
// Each value is a percent from max X/Y stored as an int. Have to bring that value down
// to a decimal before multiplying by MAX X/Y.
sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float)res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float)res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float)res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float)res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float)res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float)res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float)res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float)res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY));
// We want to commit right away, otherwise the overlay could load before this is saved.
sPrefsEditor.commit();
}
private void defaultOverlayPortrait()
{
SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
// Get screen size
Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
float maxX = outMetrics.heightPixels;
float maxY = outMetrics.widthPixels;
// Height and width changes depending on orientation. Use the larger value for height.
if (maxY < maxX)
{
float tmp = maxX;
maxX = maxY;
maxY = tmp;
}
Resources res = getResources();
String portrait = "-Portrait";
// Each value is a percent from max X/Y stored as an int. Have to bring that value down
// to a decimal before multiplying by MAX X/Y.
sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float)res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float)res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY));
// We want to commit right away, otherwise the overlay could load before this is saved.
sPrefsEditor.commit();
}
public boolean isInEditMode()
{
return mIsInEditMode;
}
}

View File

@ -0,0 +1,137 @@
/**
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_android.overlay;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;
/**
* Custom {@link BitmapDrawable} that is capable
* of storing it's own ID.
*/
public final class InputOverlayDrawableButton
{
// The ID identifying what type of button this Drawable represents.
private int mButtonType;
private int mTrackId;
private int mPreviousTouchX, mPreviousTouchY;
private int mControlPositionX, mControlPositionY;
private int mWidth;
private int mHeight;
private BitmapDrawable mDefaultStateBitmap;
private BitmapDrawable mPressedStateBitmap;
private boolean mPressedState = false;
/**
* Constructor
*
* @param res {@link Resources} instance.
* @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable.
* @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable.
* @param buttonType Identifier for this type of button.
*/
public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap,
Bitmap pressedStateBitmap, int buttonType)
{
mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap);
mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap);
mButtonType = buttonType;
mWidth = mDefaultStateBitmap.getIntrinsicWidth();
mHeight = mDefaultStateBitmap.getIntrinsicHeight();
}
/**
* Gets this InputOverlayDrawableButton's button ID.
*
* @return this InputOverlayDrawableButton's button ID.
*/
public int getId()
{
return mButtonType;
}
public void setTrackId(int trackId)
{
mTrackId = trackId;
}
public int getTrackId()
{
return mTrackId;
}
public boolean onConfigureTouch(MotionEvent event)
{
int pointerIndex = event.getActionIndex();
int fingerPositionX = (int) event.getX(pointerIndex);
int fingerPositionY = (int) event.getY(pointerIndex);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
case MotionEvent.ACTION_MOVE:
mControlPositionX += fingerPositionX - mPreviousTouchX;
mControlPositionY += fingerPositionY - mPreviousTouchY;
setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX,
getHeight() + mControlPositionY);
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
}
return true;
}
public void setPosition(int x, int y)
{
mControlPositionX = x;
mControlPositionY = y;
}
public void draw(Canvas canvas)
{
getCurrentStateBitmapDrawable().draw(canvas);
}
private BitmapDrawable getCurrentStateBitmapDrawable()
{
return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap;
}
public void setBounds(int left, int top, int right, int bottom)
{
mDefaultStateBitmap.setBounds(left, top, right, bottom);
mPressedStateBitmap.setBounds(left, top, right, bottom);
}
public Rect getBounds()
{
return mDefaultStateBitmap.getBounds();
}
public int getWidth()
{
return mWidth;
}
public int getHeight()
{
return mHeight;
}
public void setPressedState(boolean isPressed)
{
mPressedState = isPressed;
}
}

View File

@ -0,0 +1,206 @@
/**
* Copyright 2016 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_android.overlay;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;
/**
* Custom {@link BitmapDrawable} that is capable
* of storing it's own ID.
*/
public final class InputOverlayDrawableDpad
{
// The ID identifying what type of button this Drawable represents.
private int[] mButtonType = new int[4];
private int mTrackId;
private int mPreviousTouchX, mPreviousTouchY;
private int mControlPositionX, mControlPositionY;
private int mWidth;
private int mHeight;
private BitmapDrawable mDefaultStateBitmap;
private BitmapDrawable mPressedOneDirectionStateBitmap;
private BitmapDrawable mPressedTwoDirectionsStateBitmap;
private int mPressState = STATE_DEFAULT;
public static final int STATE_DEFAULT = 0;
public static final int STATE_PRESSED_UP = 1;
public static final int STATE_PRESSED_DOWN = 2;
public static final int STATE_PRESSED_LEFT = 3;
public static final int STATE_PRESSED_RIGHT = 4;
public static final int STATE_PRESSED_UP_LEFT = 5;
public static final int STATE_PRESSED_UP_RIGHT = 6;
public static final int STATE_PRESSED_DOWN_LEFT = 7;
public static final int STATE_PRESSED_DOWN_RIGHT = 8;
/**
* Constructor
*
* @param res {@link Resources} instance.
* @param defaultStateBitmap {@link Bitmap} of the default state.
* @param pressedOneDirectionStateBitmap {@link Bitmap} of the pressed state in one direction.
* @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction.
* @param buttonUp Identifier for the up button.
* @param buttonDown Identifier for the down button.
* @param buttonLeft Identifier for the left button.
* @param buttonRight Identifier for the right button.
*/
public InputOverlayDrawableDpad(Resources res,
Bitmap defaultStateBitmap,
Bitmap pressedOneDirectionStateBitmap,
Bitmap pressedTwoDirectionsStateBitmap,
int buttonUp, int buttonDown,
int buttonLeft, int buttonRight)
{
mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap);
mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap);
mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap);
mWidth = mDefaultStateBitmap.getIntrinsicWidth();
mHeight = mDefaultStateBitmap.getIntrinsicHeight();
mButtonType[0] = buttonUp;
mButtonType[1] = buttonDown;
mButtonType[2] = buttonLeft;
mButtonType[3] = buttonRight;
}
public void draw(Canvas canvas)
{
int px = mControlPositionX + (getWidth() / 2);
int py = mControlPositionY + (getHeight() / 2);
switch (mPressState)
{
case STATE_DEFAULT:
mDefaultStateBitmap.draw(canvas);
break;
case STATE_PRESSED_UP:
mPressedOneDirectionStateBitmap.draw(canvas);
break;
case STATE_PRESSED_RIGHT:
canvas.save();
canvas.rotate(90, px, py);
mPressedOneDirectionStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_DOWN:
canvas.save();
canvas.rotate(180, px, py);
mPressedOneDirectionStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_LEFT:
canvas.save();
canvas.rotate(270, px, py);
mPressedOneDirectionStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_UP_LEFT:
mPressedTwoDirectionsStateBitmap.draw(canvas);
break;
case STATE_PRESSED_UP_RIGHT:
canvas.save();
canvas.rotate(90, px, py);
mPressedTwoDirectionsStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_DOWN_RIGHT:
canvas.save();
canvas.rotate(180, px, py);
mPressedTwoDirectionsStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_DOWN_LEFT:
canvas.save();
canvas.rotate(270, px, py);
mPressedTwoDirectionsStateBitmap.draw(canvas);
canvas.restore();
break;
}
}
/**
* Gets one of the InputOverlayDrawableDpad's button IDs.
*
* @return the requested InputOverlayDrawableDpad's button ID.
*/
public int getId(int direction)
{
return mButtonType[direction];
}
public void setTrackId(int trackId)
{
mTrackId = trackId;
}
public int getTrackId()
{
return mTrackId;
}
public boolean onConfigureTouch(MotionEvent event)
{
int pointerIndex = event.getActionIndex();
int fingerPositionX = (int) event.getX(pointerIndex);
int fingerPositionY = (int) event.getY(pointerIndex);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
case MotionEvent.ACTION_MOVE:
mControlPositionX += fingerPositionX - mPreviousTouchX;
mControlPositionY += fingerPositionY - mPreviousTouchY;
setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX,
getHeight() + mControlPositionY);
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
}
return true;
}
public void setPosition(int x, int y)
{
mControlPositionX = x;
mControlPositionY = y;
}
public void setBounds(int left, int top, int right, int bottom)
{
mDefaultStateBitmap.setBounds(left, top, right, bottom);
mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom);
mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom);
}
public Rect getBounds()
{
return mDefaultStateBitmap.getBounds();
}
public int getWidth()
{
return mWidth;
}
public int getHeight()
{
return mHeight;
}
public void setState(int pressState)
{
mPressState = pressState;
}
}

View File

@ -0,0 +1,264 @@
/**
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_android.overlay;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;
/**
* Custom {@link BitmapDrawable} that is capable
* of storing it's own ID.
*/
public final class InputOverlayDrawableJoystick
{
private final int[] axisIDs = {0, 0, 0, 0};
private final float[] axises = {0f, 0f};
private int trackId = -1;
private int mJoystickType;
private int mControlPositionX, mControlPositionY;
private int mPreviousTouchX, mPreviousTouchY;
private int mWidth;
private int mHeight;
private Rect mVirtBounds;
private Rect mOrigBounds;
private BitmapDrawable mOuterBitmap;
private BitmapDrawable mDefaultStateInnerBitmap;
private BitmapDrawable mPressedStateInnerBitmap;
private BitmapDrawable mBoundsBoxBitmap;
private boolean mPressedState = false;
/**
* Constructor
*
* @param res {@link Resources} instance.
* @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick.
* @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick.
* @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick.
* @param rectOuter {@link Rect} which represents the outer joystick bounds.
* @param rectInner {@link Rect} which represents the inner joystick bounds.
* @param joystick Identifier for which joystick this is.
*/
public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter,
Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed,
Rect rectOuter, Rect rectInner, int joystick)
{
axisIDs[0] = joystick + 1; // Up
axisIDs[1] = joystick + 2; // Down
axisIDs[2] = joystick + 3; // Left
axisIDs[3] = joystick + 4; // Right
mJoystickType = joystick;
mOuterBitmap = new BitmapDrawable(res, bitmapOuter);
mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault);
mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed);
mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter);
mWidth = bitmapOuter.getWidth();
mHeight = bitmapOuter.getHeight();
setBounds(rectOuter);
mDefaultStateInnerBitmap.setBounds(rectInner);
mPressedStateInnerBitmap.setBounds(rectInner);
mVirtBounds = getBounds();
mOrigBounds = mOuterBitmap.copyBounds();
mBoundsBoxBitmap.setAlpha(0);
mBoundsBoxBitmap.setBounds(getVirtBounds());
SetInnerBounds();
}
/**
* Gets this InputOverlayDrawableJoystick's button ID.
*
* @return this InputOverlayDrawableJoystick's button ID.
*/
public int getId()
{
return mJoystickType;
}
public void draw(Canvas canvas)
{
mOuterBitmap.draw(canvas);
getCurrentStateBitmapDrawable().draw(canvas);
mBoundsBoxBitmap.draw(canvas);
}
public void TrackEvent(MotionEvent event)
{
int pointerIndex = event.getActionIndex();
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
if (getBounds().contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex)))
{
mPressedState = true;
mOuterBitmap.setAlpha(0);
mBoundsBoxBitmap.setAlpha(255);
getVirtBounds().offset((int) event.getX(pointerIndex) - getVirtBounds().centerX(),
(int) event.getY(pointerIndex) - getVirtBounds().centerY());
mBoundsBoxBitmap.setBounds(getVirtBounds());
trackId = event.getPointerId(pointerIndex);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (trackId == event.getPointerId(pointerIndex))
{
mPressedState = false;
axises[0] = axises[1] = 0.0f;
mOuterBitmap.setAlpha(255);
mBoundsBoxBitmap.setAlpha(0);
setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right,
mOrigBounds.bottom));
setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right,
mOrigBounds.bottom));
SetInnerBounds();
trackId = -1;
}
break;
}
if (trackId == -1)
return;
for (int i = 0; i < event.getPointerCount(); i++)
{
if (trackId == event.getPointerId(i))
{
float touchX = event.getX(i);
float touchY = event.getY(i);
float maxY = getVirtBounds().bottom;
float maxX = getVirtBounds().right;
touchX -= getVirtBounds().centerX();
maxX -= getVirtBounds().centerX();
touchY -= getVirtBounds().centerY();
maxY -= getVirtBounds().centerY();
final float AxisX = touchX / maxX;
final float AxisY = touchY / maxY;
axises[0] = AxisX;
axises[1] = AxisY;
SetInnerBounds();
}
}
}
public boolean onConfigureTouch(MotionEvent event)
{
int pointerIndex = event.getActionIndex();
int fingerPositionX = (int) event.getX(pointerIndex);
int fingerPositionY = (int) event.getY(pointerIndex);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = fingerPositionX - mPreviousTouchX;
int deltaY = fingerPositionY - mPreviousTouchY;
mControlPositionX += deltaX;
mControlPositionY += deltaY;
setBounds(new Rect(mControlPositionX, mControlPositionY,
mOuterBitmap.getIntrinsicWidth() + mControlPositionX,
mOuterBitmap.getIntrinsicHeight() + mControlPositionY));
setVirtBounds(new Rect(mControlPositionX, mControlPositionY,
mOuterBitmap.getIntrinsicWidth() + mControlPositionX,
mOuterBitmap.getIntrinsicHeight() + mControlPositionY));
SetInnerBounds();
setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY,
mOuterBitmap.getIntrinsicWidth() + mControlPositionX,
mOuterBitmap.getIntrinsicHeight() + mControlPositionY)));
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
}
return true;
}
public float[] getAxisValues()
{
return axises;
}
public int[] getAxisIDs()
{
return axisIDs;
}
private void SetInnerBounds()
{
int X = getVirtBounds().centerX() + (int) ((axises[0]) * (getVirtBounds().width() / 2));
int Y = getVirtBounds().centerY() + (int) ((axises[1]) * (getVirtBounds().height() / 2));
if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2))
X = getVirtBounds().centerX() + (getVirtBounds().width() / 2);
if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2))
X = getVirtBounds().centerX() - (getVirtBounds().width() / 2);
if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2))
Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2);
if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2))
Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2);
int width = mPressedStateInnerBitmap.getBounds().width() / 2;
int height = mPressedStateInnerBitmap.getBounds().height() / 2;
mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height);
mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds());
}
public void setPosition(int x, int y)
{
mControlPositionX = x;
mControlPositionY = y;
}
private BitmapDrawable getCurrentStateBitmapDrawable()
{
return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap;
}
public void setBounds(Rect bounds)
{
mOuterBitmap.setBounds(bounds);
}
public Rect getBounds()
{
return mOuterBitmap.getBounds();
}
private void setVirtBounds(Rect bounds)
{
mVirtBounds = bounds;
}
private void setOrigBounds(Rect bounds)
{
mOrigBounds = bounds;
}
private Rect getVirtBounds()
{
return mVirtBounds;
}
public int getWidth()
{
return mWidth;
}
public int getHeight()
{
return mHeight;
}
}

View File

@ -0,0 +1,233 @@
/**
* Copyright 2014 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_android.services;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.utils.Log;
import org.citra.citra_android.utils.PermissionsHandler;
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;
/**
* A service that spawns its own thread in order to copy several binary and shader files
* from the Dolphin APK to the external file system.
*/
public final class DirectoryInitializationService extends IntentService
{
public static final String BROADCAST_ACTION = "org.citra.citra_android.BROADCAST";
public static final String EXTRA_STATE = "directoryState";
private static volatile DirectoryInitializationState directoryState = null;
private static String userPath;
private static AtomicBoolean isDolphinDirectoryInitializationRunning = new AtomicBoolean(false);
public enum DirectoryInitializationState
{
DOLPHIN_DIRECTORIES_INITIALIZED,
EXTERNAL_STORAGE_PERMISSION_NEEDED,
CANT_FIND_EXTERNAL_STORAGE
}
public DirectoryInitializationService()
{
// Superclass constructor is called to name the thread on which this service executes.
super("DirectoryInitializationService");
}
public static void startService(Context context)
{
Intent intent = new Intent(context, DirectoryInitializationService.class);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent)
{
isDolphinDirectoryInitializationRunning.set(true);
if (directoryState != DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
{
if (PermissionsHandler.hasWriteAccess(this))
{
if (setDolphinUserDirectory())
{
initializeInternalStorage();
CreateUserDirectories();
NativeLibrary.CreateConfigFile();
directoryState = DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
}
else
{
directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
}
}
else
{
directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
}
}
isDolphinDirectoryInitializationRunning.set(false);
sendBroadcastState(directoryState);
}
private boolean setDolphinUserDirectory()
{
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
{
File externalPath = Environment.getExternalStorageDirectory();
if (externalPath != null)
{
userPath = externalPath.getAbsolutePath() + "/citra-emu";
Log.debug("[DirectoryInitializationService] User Dir: " + userPath);
// NativeLibrary.SetUserDirectory(userPath);
return true;
}
}
return false;
}
private void initializeInternalStorage()
{
File sysDirectory = new File(getFilesDir(), "Sys");
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
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 Dolphin that might contain outdated files. Let's (re-)extract Sys.
deleteDirectoryRecursively(sysDirectory);
copyAssetFolder("Sys", sysDirectory, true);
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 deleteDirectoryRecursively(File file)
{
if (file.isDirectory())
{
for (File child : file.listFiles())
deleteDirectoryRecursively(child);
}
file.delete();
}
public static boolean areDolphinDirectoriesReady()
{
return directoryState == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
}
public static String getUserDirectory()
{
if (directoryState == null)
{
throw new IllegalStateException("DirectoryInitializationService has to run at least once!");
}
else if (isDolphinDirectoryInitializationRunning.get())
{
throw new IllegalStateException(
"DirectoryInitializationService has to finish running first!");
}
return userPath;
}
private void sendBroadcastState(DirectoryInitializationState state)
{
Intent localIntent =
new Intent(BROADCAST_ACTION)
.putExtra(EXTRA_STATE, state);
LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
}
private void copyAsset(String asset, File output, Boolean overwrite)
{
Log.verbose("[DirectoryInitializationService] Copying File " + asset + " to " + output);
try
{
if (!output.exists() || overwrite)
{
InputStream in = getAssets().open(asset);
OutputStream out = new FileOutputStream(output);
copyFile(in, out);
in.close();
out.close();
}
}
catch (IOException e)
{
Log.error("[DirectoryInitializationService] Failed to copy asset file: " + asset +
e.getMessage());
}
}
private void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite)
{
Log.verbose("[DirectoryInitializationService] Copying Folder " + assetFolder + " to " +
outputFolder);
try
{
boolean createdFolder = false;
for (String file : getAssets().list(assetFolder))
{
if (!createdFolder)
{
outputFolder.mkdir();
createdFolder = true;
}
copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
overwrite);
copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite);
}
}
catch (IOException e)
{
Log.error("[DirectoryInitializationService] Failed to copy asset folder: " + assetFolder +
e.getMessage());
}
}
private 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);
}
}
private static native void CreateUserDirectories();
private static native void SetSysDirectory(String path);
}

View File

@ -0,0 +1,19 @@
package org.citra.citra_android.services;
import android.app.IntentService;
import android.content.Intent;
public final class USBPermService extends IntentService
{
public USBPermService()
{
super("USBPermService");
}
// Needed when extending IntentService.
// We don't care about the results of the intent handler for this.
@Override
protected void onHandleIntent(Intent intent)
{
}
}

View File

@ -0,0 +1,157 @@
package org.citra.citra_android.ui;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
/**
* Implementation from:
* https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36
*/
public final class DividerItemDecoration extends RecyclerView.ItemDecoration
{
private Drawable mDivider;
private boolean mShowFirstDivider = false;
private boolean mShowLastDivider = false;
public DividerItemDecoration(Context context, AttributeSet attrs)
{
final TypedArray a = context
.obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider});
mDivider = a.getDrawable(0);
a.recycle();
}
public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider,
boolean showLastDivider)
{
this(context, attrs);
mShowFirstDivider = showFirstDivider;
mShowLastDivider = showLastDivider;
}
public DividerItemDecoration(Drawable divider)
{
mDivider = divider;
}
public DividerItemDecoration(Drawable divider, boolean showFirstDivider,
boolean showLastDivider)
{
this(divider);
mShowFirstDivider = showFirstDivider;
mShowLastDivider = showLastDivider;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state)
{
super.getItemOffsets(outRect, view, parent, state);
if (mDivider == null)
{
return;
}
if (parent.getChildPosition(view) < 1)
{
return;
}
if (getOrientation(parent) == LinearLayoutManager.VERTICAL)
{
outRect.top = mDivider.getIntrinsicHeight();
}
else
{
outRect.left = mDivider.getIntrinsicWidth();
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)
{
if (mDivider == null)
{
super.onDrawOver(c, parent, state);
return;
}
// Initialization needed to avoid compiler warning
int left = 0, right = 0, top = 0, bottom = 0, size;
int orientation = getOrientation(parent);
int childCount = parent.getChildCount();
if (orientation == LinearLayoutManager.VERTICAL)
{
size = mDivider.getIntrinsicHeight();
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
}
else
{ //horizontal
size = mDivider.getIntrinsicWidth();
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
}
for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++)
{
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
if (orientation == LinearLayoutManager.VERTICAL)
{
top = child.getTop() - params.topMargin;
bottom = top + size;
}
else
{ //horizontal
left = child.getLeft() - params.leftMargin;
right = left + size;
}
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
// show last divider
if (mShowLastDivider && childCount > 0)
{
View child = parent.getChildAt(childCount - 1);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
if (orientation == LinearLayoutManager.VERTICAL)
{
top = child.getBottom() + params.bottomMargin;
bottom = top + size;
}
else
{ // horizontal
left = child.getRight() + params.rightMargin;
right = left + size;
}
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
private int getOrientation(RecyclerView parent)
{
if (parent.getLayoutManager() instanceof LinearLayoutManager)
{
LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
return layoutManager.getOrientation();
}
else
{
throw new IllegalStateException(
"DividerItemDecoration can only be used with a LinearLayoutManager.");
}
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_android.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
/**
* Work around a bug with the nVidia Shield.
*/
public final class NVidiaShieldWorkaroundView extends View
{
public NVidiaShieldWorkaroundView(Context context, AttributeSet attrs)
{
super(context, attrs);
// Setting this seems to workaround the bug
setWillNotDraw(false);
}
}

View File

@ -0,0 +1,6 @@
package org.citra.citra_android.ui.input.gamecube;
public class ControllerFragment
{
}

View File

@ -0,0 +1,6 @@
package org.citra.citra_android.ui.input.gamecube;
public class ControllerFragmentPresenter
{
}

View File

@ -0,0 +1,6 @@
package org.citra.citra_android.ui.input.gamecube;
public interface ControllerFragmentView
{
}

View File

@ -0,0 +1,82 @@
package org.citra.citra_android.ui.main;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.widget.TitleViewAdapter;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.citra.citra_android.R;
public class CustomTitleView extends LinearLayout implements TitleViewAdapter.Provider
{
private final TextView mTitleView;
private final View mBadgeView;
private final TitleViewAdapter mTitleViewAdapter = new TitleViewAdapter()
{
@Override
public View getSearchAffordanceView()
{
return null;
}
@Override
public void setTitle(CharSequence titleText)
{
CustomTitleView.this.setTitle(titleText);
}
@Override
public void setBadgeDrawable(Drawable drawable)
{
CustomTitleView.this.setBadgeDrawable(drawable);
}
};
public CustomTitleView(Context context)
{
this(context, null);
}
public CustomTitleView(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
View root = LayoutInflater.from(context).inflate(R.layout.tv_title, this);
mTitleView = root.findViewById(R.id.title);
mBadgeView = root.findViewById(R.id.badge);
}
public void setTitle(CharSequence title)
{
if (title != null)
{
mTitleView.setText(title);
mTitleView.setVisibility(View.VISIBLE);
mBadgeView.setVisibility(View.VISIBLE);
}
}
public void setBadgeDrawable(Drawable drawable)
{
if (drawable != null)
{
mTitleView.setVisibility(View.GONE);
mBadgeView.setVisibility(View.VISIBLE);
}
}
@Override
public TitleViewAdapter getTitleViewAdapter()
{
return mTitleViewAdapter;
}
}

View File

@ -0,0 +1,213 @@
package org.citra.citra_android.ui.main;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import org.citra.citra_android.R;
import org.citra.citra_android.model.GameProvider;
import org.citra.citra_android.services.DirectoryInitializationService;
import org.citra.citra_android.ui.platform.PlatformGamesFragment;
import org.citra.citra_android.ui.platform.PlatformGamesView;
import org.citra.citra_android.ui.settings.SettingsActivity;
import org.citra.citra_android.utils.AddDirectoryHelper;
import org.citra.citra_android.utils.FileBrowserHelper;
import org.citra.citra_android.utils.PermissionsHandler;
import org.citra.citra_android.utils.StartupHandler;
/**
* 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 FloatingActionButton mFab;
private MainPresenter mPresenter = new MainPresenter(this);
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViews();
setSupportActionBar(mToolbar);
mFrameLayoutId = R.id.games_platform_frame;
// Set up the FAB.
mFab.setOnClickListener(view -> mPresenter.onFabClick());
mPresenter.onCreate();
// Stuff in this block only happens when this activity is newly created (i.e. not a rotation)
if (savedInstanceState == null)
StartupHandler.HandleInit(this);
if (PermissionsHandler.hasWriteAccess(this))
{
mPlatformGamesFragment = new PlatformGamesFragment();
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
.commit();
}
}
@Override
protected void onResume()
{
super.onResume();
mPresenter.addDirIfNeeded(new AddDirectoryHelper(this));
}
// TODO: Replace with a ButterKnife injection.
private void findViews()
{
mToolbar = findViewById(R.id.toolbar_main);
mFab = findViewById(R.id.button_add_directory);
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_game_grid, menu);
return true;
}
/**
* MainView
*/
@Override
public void setVersionString(String version)
{
mToolbar.setSubtitle(version);
}
@Override
public void refresh()
{
getContentResolver().insert(GameProvider.URI_REFRESH, null);
refreshFragment();
}
@Override
public void refreshFragmentScreenshot(int fragmentPosition)
{
// Invalidate Picasso image so that the new screenshot is animated in.
PlatformGamesView fragment = getPlatformGamesView();
if (fragment != null)
{
fragment.refreshScreenshotAtPosition(fragmentPosition);
}
}
@Override
public void launchSettingsActivity(String menuTag)
{
SettingsActivity.launch(this, menuTag, "");
}
@Override
public void launchFileListActivity()
{
FileBrowserHelper.openDirectoryPicker(this);
}
@Override
public void showGames(Cursor games)
{
// no-op. Handled by PlatformGamesFragment.
}
/**
* @param requestCode An int describing whether the Activity that is returning did so successfully.
* @param resultCode An int describing what Activity is giving us this callback.
* @param result The information the returning Activity is providing us.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result)
{
switch (requestCode)
{
case MainPresenter.REQUEST_ADD_DIRECTORY:
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK)
{
mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result));
}
break;
case MainPresenter.REQUEST_EMULATE_GAME:
mPresenter.refreshFragmentScreenshot(resultCode);
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{
switch (requestCode)
{
case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
{
DirectoryInitializationService.startService(this);
mPlatformGamesFragment = new PlatformGamesFragment();
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
.commit();
}
else
{
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
/**
* 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();
}
}
@Nullable
private PlatformGamesView getPlatformGamesView()
{
return (PlatformGamesView) getSupportFragmentManager().findFragmentById(mFrameLayoutId);
}
}

View File

@ -0,0 +1,90 @@
package org.citra.citra_android.ui.main;
import org.citra.citra_android.BuildConfig;
import org.citra.citra_android.DolphinApplication;
import org.citra.citra_android.R;
import org.citra.citra_android.model.GameDatabase;
import org.citra.citra_android.utils.AddDirectoryHelper;
import org.citra.citra_android.utils.SettingsFile;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public final class MainPresenter
{
public static final int REQUEST_ADD_DIRECTORY = 1;
public static final int REQUEST_EMULATE_GAME = 2;
private final MainView mView;
private String mDirToAdd;
public MainPresenter(MainView view)
{
mView = view;
}
public void onCreate()
{
String versionName = BuildConfig.VERSION_NAME;
mView.setVersionString(versionName);
}
public void onFabClick()
{
mView.launchFileListActivity();
}
public boolean handleOptionSelection(int itemId)
{
switch (itemId)
{
case R.id.menu_settings_core:
mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG);
return true;
case R.id.menu_refresh:
GameDatabase databaseHelper = DolphinApplication.databaseHelper;
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
mView.refresh();
return true;
case R.id.button_add_directory:
mView.launchFileListActivity();
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 refreshFragmentScreenshot(int resultCode)
{
mView.refreshFragmentScreenshot(resultCode);
}
public void loadGames()
{
GameDatabase databaseHelper = DolphinApplication.databaseHelper;
databaseHelper.getGames()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(games -> mView.showGames(games));
}
}

View File

@ -0,0 +1,45 @@
package org.citra.citra_android.ui.main;
import android.database.Cursor;
/**
* 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();
/**
* Tell the view to tell the currently displayed {@link android.support.v4.app.Fragment}
* to refresh the screenshot at the given position in its list of games.
*
* @param fragmentPosition An index corresponding to the list or grid of games.
*/
void refreshFragmentScreenshot(int fragmentPosition);
void launchSettingsActivity(String menuTag);
void launchFileListActivity();
/**
* 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);
}

View File

@ -0,0 +1,269 @@
package org.citra.citra_android.ui.main;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v17.leanback.app.BrowseFragment;
import android.support.v17.leanback.app.BrowseSupportFragment;
import android.support.v17.leanback.database.CursorMapper;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.CursorObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.widget.Toast;
import org.citra.citra_android.R;
import org.citra.citra_android.activities.EmulationActivity;
import org.citra.citra_android.adapters.GameRowPresenter;
import org.citra.citra_android.adapters.SettingsRowPresenter;
import org.citra.citra_android.model.Game;
import org.citra.citra_android.model.TvSettingsItem;
import org.citra.citra_android.services.DirectoryInitializationService;
import org.citra.citra_android.ui.settings.SettingsActivity;
import org.citra.citra_android.utils.AddDirectoryHelper;
import org.citra.citra_android.utils.FileBrowserHelper;
import org.citra.citra_android.utils.PermissionsHandler;
import org.citra.citra_android.utils.StartupHandler;
import org.citra.citra_android.viewholders.TvGameViewHolder;
public final class TvMainActivity extends FragmentActivity implements MainView
{
private MainPresenter mPresenter = new MainPresenter(this);
private BrowseSupportFragment mBrowseFragment;
private ArrayObjectAdapter mRowsAdapter;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tv_main);
setupUI();
mPresenter.onCreate();
// Stuff in this block only happens when this activity is newly created (i.e. not a rotation)
if (savedInstanceState == null)
StartupHandler.HandleInit(this);
}
@Override
protected void onResume()
{
super.onResume();
mPresenter.addDirIfNeeded(new AddDirectoryHelper(this));
}
void setupUI()
{
final FragmentManager fragmentManager = getSupportFragmentManager();
mBrowseFragment = new BrowseSupportFragment();
fragmentManager
.beginTransaction()
.add(R.id.content, mBrowseFragment, "BrowseFragment")
.commit();
// Set display parameters for the BrowseFragment
mBrowseFragment.setHeadersState(BrowseFragment.HEADERS_ENABLED);
mBrowseFragment.setBrandColor(ContextCompat.getColor(this, R.color.citra_orange_dark));
buildRowsAdapter();
mBrowseFragment.setOnItemViewClickedListener(
(itemViewHolder, item, rowViewHolder, row) ->
{
// Special case: user clicked on a settings row item.
if (item instanceof TvSettingsItem)
{
TvSettingsItem settingsItem = (TvSettingsItem) item;
mPresenter.handleOptionSelection(settingsItem.getItemId());
}
else
{
TvGameViewHolder holder = (TvGameViewHolder) itemViewHolder;
// Start the emulation activity and send the path of the clicked ISO to it.
EmulationActivity.launch(TvMainActivity.this,
holder.path,
holder.title,
holder.screenshotPath,
-1,
holder.imageScreenshot);
}
});
}
/**
* MainView
*/
@Override
public void setVersionString(String version)
{
mBrowseFragment.setTitle(version);
}
@Override
public void refresh()
{
recreate();
}
@Override
public void refreshFragmentScreenshot(int fragmentPosition)
{
mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size());
}
@Override
public void launchSettingsActivity(String menuTag)
{
SettingsActivity.launch(this, menuTag, "");
}
@Override
public void launchFileListActivity()
{
FileBrowserHelper.openDirectoryPicker(this);
}
@Override
public void showGames(Cursor games)
{
ListRow row = buildGamesRow(games);
// Add row to the adapter only if it is not empty.
if (row != null)
{
mRowsAdapter.add(games);
}
}
/**
* Callback from AddDirectoryActivity. Applies any changes necessary to the GameGridActivity.
*
* @param requestCode An int describing whether the Activity that is returning did so successfully.
* @param resultCode An int describing what Activity is giving us this callback.
* @param result The information the returning Activity is providing us.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result)
{
switch (requestCode)
{
case MainPresenter.REQUEST_ADD_DIRECTORY:
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK)
{
mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result));
}
break;
case MainPresenter.REQUEST_EMULATE_GAME:
mPresenter.refreshFragmentScreenshot(resultCode);
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{
switch (requestCode)
{
case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
{
DirectoryInitializationService.startService(this);
loadGames();
}
else
{
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
private void buildRowsAdapter()
{
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
if (PermissionsHandler.hasWriteAccess(this))
{
loadGames();
}
mRowsAdapter.add(buildSettingsRow());
mBrowseFragment.setAdapter(mRowsAdapter);
}
private void loadGames()
{
mPresenter.loadGames();
}
private ListRow buildGamesRow(Cursor games)
{
// Create an adapter for this row.
CursorObjectAdapter row = new CursorObjectAdapter(new GameRowPresenter());
// If cursor is empty, don't return a Row.
if (!games.moveToFirst())
{
return null;
}
row.changeCursor(games);
row.setMapper(new CursorMapper()
{
@Override
protected void bindColumns(Cursor cursor)
{
// No-op? Not sure what this does.
}
@Override
protected Object bind(Cursor cursor)
{
return Game.fromCursor(cursor);
}
});
// Create the row, passing it the filled adapter and the header, and give it to the master adapter.
return new ListRow(null, row);
}
private ListRow buildSettingsRow()
{
ArrayObjectAdapter rowItems = new ArrayObjectAdapter(new SettingsRowPresenter());
rowItems.add(new TvSettingsItem(R.id.menu_settings_core,
R.drawable.ic_settings_core_tv,
R.string.grid_menu_core_settings));
rowItems.add(new TvSettingsItem(R.id.button_add_directory,
R.drawable.ic_add_tv,
R.string.add_directory_title));
rowItems.add(new TvSettingsItem(R.id.menu_refresh,
R.drawable.ic_refresh_tv,
R.string.grid_menu_refresh));
// Create a header for this row.
HeaderItem header =
new HeaderItem(R.string.preferences_settings, getString(R.string.preferences_settings));
return new ListRow(header, rowItems);
}
}

View File

@ -0,0 +1,88 @@
package org.citra.citra_android.ui.platform;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.citra.citra_android.R;
import org.citra.citra_android.adapters.GameAdapter;
public final class PlatformGamesFragment extends Fragment implements PlatformGamesView
{
private static final String ARG_PLATFORM = "platform";
private PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this);
private GameAdapter mAdapter;
private RecyclerView mRecyclerView;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
}
@Nullable
@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;
}
@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);
mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8));
}
@Override
public void refreshScreenshotAtPosition(int position)
{
mAdapter.notifyItemChanged(position);
}
@Override
public void refresh()
{
mPresenter.refresh();
}
@Override
public void onItemClick(String gameId)
{
// No-op for now
}
@Override
public void showGames(Cursor games)
{
if (mAdapter != null)
{
mAdapter.swapCursor(games);
}
}
private void findViews(View root)
{
mRecyclerView = root.findViewById(R.id.grid_games);
}
}

View File

@ -0,0 +1,47 @@
package org.citra.citra_android.ui.platform;
import org.citra.citra_android.DolphinApplication;
import org.citra.citra_android.model.GameDatabase;
import org.citra.citra_android.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 = DolphinApplication.databaseHelper;
databaseHelper.getGames()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(games ->
{
Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...");
mView.showGames(games);
});
}
}

View File

@ -0,0 +1,38 @@
package org.citra.citra_android.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();
/**
* Tell the view that a certain game's screenshot has been updated,
* and should be redrawn on-screen.
*
* @param position The index of the game that should be redrawn.
*/
void refreshScreenshotAtPosition(int position);
/**
* Pass a click event to the view's Presenter. Typically called from the
* view's list adapter.
*
* @param gameId The ID of the game that was clicked.
*/
void onItemClick(String gameId);
/**
* 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);
}

View File

@ -0,0 +1,266 @@
package org.citra.citra_android.ui.settings;
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.support.v4.app.FragmentTransaction;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.SettingSection;
import org.citra.citra_android.services.DirectoryInitializationService;
import org.citra.citra_android.utils.DirectoryStateReceiver;
import org.citra.citra_android.utils.Log;
import java.util.ArrayList;
import java.util.HashMap;
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView
{
private static final String ARG_FILE_NAME = "file_name";
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_FILE_NAME, menuTag);
settings.putExtra(ARG_GAME_ID, gameId);
context.startActivity(settings);
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
Intent launcher = getIntent();
String filename = launcher.getStringExtra(ARG_FILE_NAME);
String gameID = launcher.getStringExtra(ARG_GAME_ID);
mPresenter.onCreate(savedInstanceState, filename, gameID);
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_settings, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
return mPresenter.handleOptionsItem(item.getItemId());
}
@Override
protected void onSaveInstanceState(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());
}
@Override
public void onBackPressed()
{
mPresenter.onBackPressed();
}
@Override
public void showSettingsFragment(String menuTag, boolean addToStack, String gameID)
{
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if (addToStack)
{
if (areSystemAnimationsEnabled())
{
transaction.setCustomAnimations(
R.animator.settings_enter,
R.animator.settings_exit,
R.animator.settings_pop_enter,
R.animator.setttings_pop_exit);
}
transaction.addToBackStack(null);
mPresenter.addToStack();
}
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);
DirectoryInitializationService.startService(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 HashMap<String, SettingSection> getSettings(int file)
{
return mPresenter.getSettings(file);
}
@Override
public void setSettings(ArrayList<HashMap<String, SettingSection>> settings)
{
mPresenter.setSettings(settings);
}
@Override
public void onSettingsFileLoaded(ArrayList<HashMap<String, SettingSection>> 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)
{
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
@Override
public void popBackStack()
{
getSupportFragmentManager().popBackStackImmediate();
}
@Override
public void onSettingChanged()
{
mPresenter.onSettingChanged();
}
@Override
public void onGcPadSettingChanged(String key, int value)
{
mPresenter.onGcPadSettingChanged(key, value);
}
@Override
public void onWiimoteSettingChanged(String section, int value)
{
mPresenter.onWiimoteSettingChanged(section, value);
}
@Override
public void onExtensionSettingChanged(String key, int value)
{
mPresenter.onExtensionSettingChanged(key, value);
}
private SettingsFragment getFragment()
{
return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
}
}

View File

@ -0,0 +1,220 @@
package org.citra.citra_android.ui.settings;
import android.content.IntentFilter;
import android.os.Bundle;
import android.text.TextUtils;
import org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.SettingSection;
import org.citra.citra_android.services.DirectoryInitializationService;
import org.citra.citra_android.services.DirectoryInitializationService.DirectoryInitializationState;
import org.citra.citra_android.utils.DirectoryStateReceiver;
import org.citra.citra_android.utils.Log;
import org.citra.citra_android.utils.SettingsFile;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
public final class SettingsActivityPresenter
{
private static final String KEY_SHOULD_SAVE = "should_save";
private SettingsActivityView mView;
private ArrayList<HashMap<String, SettingSection>> mSettings = new ArrayList<>();
private int mStackCount;
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()
{
prepareDolphinDirectoriesIfNeeded();
}
void loadSettingsUI()
{
if (mSettings.isEmpty())
{
if (!TextUtils.isEmpty(gameId))
{
mSettings.add(SettingsFile.SETTINGS_DOLPHIN, SettingsFile.readFile("../GameSettings/" + gameId, mView));
}
else
{
mSettings.add(SettingsFile.SETTINGS_DOLPHIN, SettingsFile.readFile(SettingsFile.FILE_NAME_CONFIG, mView));
}
}
mView.showSettingsFragment(menuTag, false, gameId);
mView.onSettingsFileLoaded(mSettings);
}
private void prepareDolphinDirectoriesIfNeeded()
{
File configFile = new File(DirectoryInitializationService.getUserDirectory() + "/config/"+SettingsFile.FILE_NAME_CONFIG + ".ini");
if(!configFile.exists()) {
}
if (DirectoryInitializationService.areDolphinDirectoriesReady()) {
loadSettingsUI();
} else {
mView.showLoading();
IntentFilter statusIntentFilter = new IntentFilter(
DirectoryInitializationService.BROADCAST_ACTION);
directoryStateReceiver =
new DirectoryStateReceiver(directoryInitializationState ->
{
if (directoryInitializationState == DirectoryInitializationState.DOLPHIN_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(ArrayList<HashMap<String, SettingSection>> settings)
{
mSettings = settings;
}
public HashMap<String, SettingSection> getSettings(int file)
{
return mSettings.get(file);
}
public void onStop(boolean finishing)
{
if (directoryStateReceiver != null)
{
mView.stopListeningToDirectoryInitializationService(directoryStateReceiver);
directoryStateReceiver = null;
}
if (mSettings != null && finishing && mShouldSave)
{
if (!TextUtils.isEmpty(gameId)) {
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
// Needed workaround for now due to an odd bug in how it handles saving two different settings sections to the same file. It won't save GFX settings if it follows the normal saving pattern
if (menuTag.equals("Dolphin"))
{
SettingsFile.saveFile("../GameSettings/" + gameId, mSettings.get(SettingsFile.SETTINGS_DOLPHIN), mView);
}
mView.showToastMessage("Saved settings for " + gameId);
} else {
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
SettingsFile.saveFile(SettingsFile.FILE_NAME_CONFIG, mSettings.get(SettingsFile.SETTINGS_DOLPHIN), mView);
mView.showToastMessage("Saved settings to INI files");
}
}
}
public void addToStack()
{
mStackCount++;
}
public void onBackPressed()
{
if (mStackCount > 0)
{
mView.popBackStack();
mStackCount--;
}
else
{
mView.finish();
}
}
public boolean handleOptionsItem(int itemId)
{
switch (itemId)
{
case R.id.menu_save_exit:
mView.finish();
return true;
}
return false;
}
public void onSettingChanged()
{
mShouldSave = true;
}
public void saveState(Bundle outState)
{
outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave);
}
public void onGcPadSettingChanged(String key, int value)
{
if (value != 0) // Not disabled
{
mView.showSettingsFragment(key + (value / 6), true, gameId);
}
}
public void onWiimoteSettingChanged(String section, int value)
{
switch (value)
{
case 1:
mView.showSettingsFragment(section, true, gameId);
break;
case 2:
mView.showToastMessage("Please make sure Continuous Scanning is enabled in Core Settings.");
break;
}
}
public void onExtensionSettingChanged(String key, int value)
{
if (value != 0) // None
{
mView.showSettingsFragment(key + value, true, gameId);
}
}
}

View File

@ -0,0 +1,140 @@
package org.citra.citra_android.ui.settings;
import android.content.IntentFilter;
import org.citra.citra_android.model.settings.SettingSection;
import org.citra.citra_android.utils.DirectoryStateReceiver;
import java.util.ArrayList;
import java.util.HashMap;
/**
* 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.
*
* @param file The settings file to load.
* @return A possibly null HashMap of Settings.
*/
HashMap<String, SettingSection> getSettings(int file);
/**
* 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(ArrayList<HashMap<String, SettingSection>> settings);
/**
* Called when an asynchronous load operation completes.
*
* @param settings The (possibly null) result of the ini load operation.
*/
void onSettingsFileLoaded(ArrayList<HashMap<String, SettingSection>> 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.
*/
void showToastMessage(String message);
/**
* Show the previous fragment.
*/
void popBackStack();
/**
* 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();
/**
* Called by a containing Fragment to tell the containing Activity that a GCPad's setting
* was modified.
*
* @param key Identifier for the GCPad that was modified.
* @param value New setting for the GCPad.
*/
void onGcPadSettingChanged(String key, int value);
/**
* Called by a containing Fragment to tell the containing Activity that a Wiimote's setting
* was modified.
*
* @param section Identifier for Wiimote that was modified; Wiimotes are identified by their section,
* not their key.
* @param value New setting for the Wiimote.
*/
void onWiimoteSettingChanged(String section, int value);
/**
* Called by a containing Fragment to tell the containing Activity that an extension setting
* was modified.
*
* @param key Identifier for the extension that was modified.
* @param value New setting for the extension.
*/
void onExtensionSettingChanged(String key, int value);
/**
* 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 DirectoryInitializationService and listen for the result.
*
* @param receiver the broadcast receiver for the DirectoryInitializationService
* @param filter the Intent broadcasts to be received.
*/
void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter);
/**
* Stop listening to the DirectoryInitializationService.
*
* @param receiver The broadcast receiver to unregister.
*/
void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver);
}

View File

@ -0,0 +1,402 @@
package org.citra.citra_android.ui.settings;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v17.leanback.widget.picker.TimePicker;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.DatePicker;
import android.widget.SeekBar;
import android.widget.TextView;
import org.citra.citra_android.R;
import org.citra.citra_android.dialogs.MotionAlertDialog;
import org.citra.citra_android.model.settings.FloatSetting;
import org.citra.citra_android.model.settings.IntSetting;
import org.citra.citra_android.model.settings.StringSetting;
import org.citra.citra_android.model.settings.view.CheckBoxSetting;
import org.citra.citra_android.model.settings.view.DateTimeSetting;
import org.citra.citra_android.model.settings.view.InputBindingSetting;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.model.settings.view.SingleChoiceSetting;
import org.citra.citra_android.model.settings.view.SliderSetting;
import org.citra.citra_android.model.settings.view.SubmenuSetting;
import org.citra.citra_android.ui.settings.viewholder.CheckBoxSettingViewHolder;
import org.citra.citra_android.ui.settings.viewholder.DateTimeViewHolder;
import org.citra.citra_android.ui.settings.viewholder.HeaderViewHolder;
import org.citra.citra_android.ui.settings.viewholder.InputBindingSettingViewHolder;
import org.citra.citra_android.ui.settings.viewholder.SettingViewHolder;
import org.citra.citra_android.ui.settings.viewholder.SingleChoiceViewHolder;
import org.citra.citra_android.ui.settings.viewholder.SliderViewHolder;
import org.citra.citra_android.ui.settings.viewholder.SubmenuViewHolder;
import org.citra.citra_android.utils.Log;
import org.citra.citra_android.utils.SettingsFile;
import java.util.ArrayList;
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder>
implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener
{
private SettingsFragmentView mView;
private Context mContext;
private ArrayList<SettingsItem> mSettings;
private SettingsItem mClickedItem;
private int mSeekbarProgress;
private AlertDialog mDialog;
private TextView mTextSliderValue;
public SettingsAdapter(SettingsFragmentView view, Context context)
{
mView = view;
mContext = context;
}
@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:
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);
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(SingleChoiceSetting item)
{
mClickedItem = item;
int value = getSelectionForSingleChoiceValue(item);
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
builder.setTitle(item.getNameId());
builder.setSingleChoiceItems(item.getChoicesId(), value, this);
mDialog = builder.show();
}
public void onDateTimeClick(DateTimeSetting item){
mClickedItem = item;
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
View view = inflater.inflate(R.layout.sysclock_datetime_picker, null);
DatePicker dp = (DatePicker) view.findViewById(R.id.date_picker);
TimePicker tp = (TimePicker) 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.setIs24Hour(true);
tp.setHour(Integer.parseInt(settingValue.substring(11, 12)));
tp.setMinute(Integer.parseInt(settingValue.substring(14, 15)));
DialogInterface.OnClickListener ok = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int 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";
mView.putSetting(new StringSetting(item.getKey(), item.getSection(), item.getFile(), datetime));
mView.onSettingChanged();
mClickedItem = null;
mSeekbarProgress = -1;
closeDialog();
}
};
DialogInterface.OnClickListener cancel = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
closeDialog();
}
};
builder.setView(view);
builder.setPositiveButton("Set", ok);
builder.setNegativeButton("Cancel", cancel);
mDialog = builder.show();
}
public void onSliderClick(SliderSetting item)
{
mClickedItem = item;
mSeekbarProgress = item.getSelectedValue();
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
View view = inflater.inflate(R.layout.dialog_seekbar, null);
builder.setTitle(item.getNameId());
builder.setView(view);
builder.setPositiveButton(R.string.ok, this);
builder.setNegativeButton(R.string.cancel, this);
mDialog = builder.show();
mTextSliderValue = view.findViewById(R.id.text_value);
mTextSliderValue.setText(String.valueOf(mSeekbarProgress));
TextView units = view.findViewById(R.id.text_units);
units.setText(item.getUnits());
SeekBar seekbar = view.findViewById(R.id.seekbar);
seekbar.setMax(item.getMax());
seekbar.setProgress(mSeekbarProgress);
seekbar.setOnSeekBarChangeListener(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);
dialog.setMessage(String.format(mContext.getString(R.string.input_binding_descrip), mContext.getString(item.getNameId())));
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(R.string.cancel), this);
dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) ->
{
item.setValue("");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(item.getKey());
editor.apply();
});
dialog.setOnDismissListener(dialog1 ->
{
StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getFile(), item.getValue());
notifyItemChanged(position);
if (setting != null)
{
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);
// 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 SliderSetting)
{
SliderSetting sliderSetting = (SliderSetting) mClickedItem;
if (sliderSetting.getSetting() instanceof FloatSetting)
{
float value;
if (sliderSetting.getKey().equals(SettingsFile.KEY_FRAME_LIMIT))
{
value = mSeekbarProgress / 100.0f;
}
else
{
value = (float) mSeekbarProgress;
}
FloatSetting setting = sliderSetting.setSelectedValue(value);
if (setting != null)
{
mView.putSetting(setting);
}
}
else
{
IntSetting setting = sliderSetting.setSelectedValue(mSeekbarProgress);
if (setting != null)
{
mView.putSetting(setting);
}
}
}
mView.onSettingChanged();
mClickedItem = null;
mSeekbarProgress = -1;
}
public void closeDialog()
{
if (mDialog != null)
{
mDialog.dismiss();
mDialog = null;
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
mSeekbarProgress = progress;
mTextSliderValue.setText(String.valueOf(mSeekbarProgress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar)
{
}
@Override
public void onStopTrackingTouch(SeekBar seekBar)
{
}
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 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;
}
}

View File

@ -0,0 +1,189 @@
package org.citra.citra_android.ui.settings;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.Setting;
import org.citra.citra_android.model.settings.SettingSection;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.ui.DividerItemDecoration;
import org.citra.citra_android.utils.SettingsFile;
import java.util.ArrayList;
import java.util.HashMap;
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;
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(Context context)
{
super.onAttach(context);
mActivity = (SettingsActivityView) context;
mPresenter.onAttach();
}
/**
* This version of onAttach is needed for versions below Marshmallow.
*
* @param activity
*/
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
mActivity = (SettingsActivityView) activity;
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());
RecyclerView recyclerView = view.findViewById(R.id.list_settings);
recyclerView.setAdapter(mAdapter);
recyclerView.setLayoutManager(manager);
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
SettingsActivityView activity = (SettingsActivityView) getActivity();
ArrayList<HashMap<String, SettingSection>> settings = new ArrayList<>();
settings.add(SettingsFile.SETTINGS_DOLPHIN, activity.getSettings(SettingsFile.SETTINGS_DOLPHIN));
mPresenter.onViewCreated(settings);
}
@Override
public void onDetach()
{
super.onDetach();
mActivity = null;
if (mAdapter != null)
{
mAdapter.closeDialog();
}
}
@Override
public void onSettingsFileLoaded(ArrayList<HashMap<String, SettingSection>> settings)
{
mPresenter.setSettings(settings);
}
@Override
public void passSettingsToActivity(ArrayList<HashMap<String, SettingSection>> 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)
{
mActivity.showToastMessage(message);
}
@Override
public void putSetting(Setting setting)
{
mPresenter.putSetting(setting);
}
@Override
public void onSettingChanged()
{
mActivity.onSettingChanged();
}
@Override
public void onGcPadSettingChanged(String key, int value)
{
mActivity.onGcPadSettingChanged(key, value);
}
@Override
public void onWiimoteSettingChanged(String section, int value)
{
mActivity.onWiimoteSettingChanged(section, value);
}
@Override
public void onExtensionSettingChanged(String key, int value)
{
mActivity.onExtensionSettingChanged(key, value);
}
}

View File

@ -0,0 +1,297 @@
package org.citra.citra_android.ui.settings;
import android.text.TextUtils;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.IntSetting;
import org.citra.citra_android.model.settings.Setting;
import org.citra.citra_android.model.settings.SettingSection;
import org.citra.citra_android.model.settings.view.CheckBoxSetting;
import org.citra.citra_android.model.settings.view.DateTimeSetting;
import org.citra.citra_android.model.settings.view.HeaderSetting;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.model.settings.view.SingleChoiceSetting;
import org.citra.citra_android.model.settings.view.SliderSetting;
import org.citra.citra_android.utils.EGLHelper;
import org.citra.citra_android.utils.SettingsFile;
import java.util.ArrayList;
import java.util.HashMap;
public final class SettingsFragmentPresenter
{
private SettingsFragmentView mView;
private String mMenuTag;
private String mGameID;
private ArrayList<HashMap<String, SettingSection>> mSettings;
private ArrayList<SettingsItem> mSettingsList;
private int mControllerNumber;
private int mControllerType;
public SettingsFragmentPresenter(SettingsFragmentView view)
{
mView = view;
}
public void onCreate(String menuTag, String gameId)
{
mGameID = gameId;
mMenuTag = menuTag;
}
public void onViewCreated(ArrayList<HashMap<String, SettingSection>> 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.get(setting.getFile()).get(setting.getSection()).putSetting(setting);
}
public void loadDefaultSettings()
{
loadSettingsList();
}
public void setSettings(ArrayList<HashMap<String, SettingSection>> settings)
{
if (mSettingsList == null && settings != null)
{
mSettings = settings;
loadSettingsList();
}
else
{
mView.showSettingsList(mSettingsList);
}
}
private void loadSettingsList()
{
if (!TextUtils.isEmpty(mGameID))
{
mView.getActivity().setTitle("Game Settings: " + mGameID);
}
ArrayList<SettingsItem> sl = new ArrayList<>();
switch (mMenuTag)
{
case SettingsFile.FILE_NAME_CONFIG:
addCoreSettings(sl);
sl.add(new HeaderSetting(null, null, R.string.video_backend, 0));
addGraphicsSettings(sl);
break;
default:
mView.showToastMessage("Unimplemented menu");
return;
}
mSettingsList = sl;
mView.showSettingsList(mSettingsList);
}
private void addCoreSettings(ArrayList<SettingsItem> sl)
{
Setting useCpuJit = null;
Setting audioStretch = null;
Setting region = null;
Setting systemClock = null;
Setting dateTime = null;
if (!mSettings.get(SettingsFile.SETTINGS_DOLPHIN).isEmpty())
{
useCpuJit = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_CORE).getSetting(SettingsFile.KEY_CPU_JIT);
audioStretch = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_AUDIO).getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING);
region = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_SYSTEM).getSetting(SettingsFile.KEY_REGION_VALUE);
systemClock = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_SYSTEM).getSetting(SettingsFile.KEY_INIT_CLOCK);
dateTime = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_SYSTEM).getSetting(SettingsFile.KEY_INIT_TIME);
}
else
{
mView.passSettingsToActivity(mSettings);
}
String defaultCpuCore = System.getProperty("os.arch");
switch (defaultCpuCore)
{
case "x86_64":
sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, SettingsFile.SECTION_CORE,
SettingsFile.SETTINGS_DOLPHIN, R.string.cpu_jit, 0, true, useCpuJit));
break;
case "aarch64":
default:
break;
}
sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, SettingsFile.SECTION_SYSTEM,SettingsFile.SETTINGS_DOLPHIN, R.string.region, 0, R.array.regionNames, R.array.regionValues, -1, region));
sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, SettingsFile.SECTION_AUDIO, SettingsFile.SETTINGS_DOLPHIN, R.string.audio_stretch, R.string.audio_stretch_description, false, audioStretch));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, SettingsFile.SECTION_SYSTEM, SettingsFile.SETTINGS_DOLPHIN, R.string.init_clock, R.string.init_clock_descrip, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock));
sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, SettingsFile.SECTION_SYSTEM, SettingsFile.SETTINGS_DOLPHIN, R.string.init_time, R.string.init_time_descrip, "2000-01-01 00:00:01", dateTime));
}
private void addGraphicsSettings(ArrayList<SettingsItem> sl)
{
Setting hardwareRenderer = null;
Setting hardwareShader = null;
Setting shadersAccurateMul = null;
Setting shadersAccurateGs = null;
Setting shaderJitEnable = null;
Setting resolutionFactor = null;
Setting vsyncEnable = null;
Setting frameLimitEnable = null;
Setting frameLimitValue = null;
Setting stereoscopyEnable = null;
Setting stereoscopyDepth = null;
if (!mSettings.get(SettingsFile.SETTINGS_DOLPHIN).isEmpty())
{
hardwareRenderer = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_HW_RENDERER);
hardwareShader = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_HW_SHADER);
shadersAccurateMul = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL);
shadersAccurateGs = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_SHADERS_ACCURATE_GS);
shaderJitEnable = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_USE_SHADER_JIT);
resolutionFactor = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_RESOLUTION_FACTOR);
vsyncEnable = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_USE_VSYNC);
frameLimitEnable = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED);
frameLimitValue = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_FRAME_LIMIT);
stereoscopyEnable = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_STEREOSCOPY);
stereoscopyDepth = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_RENDERER).getSetting(SettingsFile.KEY_FACTOR_3D);
}
else
{
mView.passSettingsToActivity(mSettings);
}
if (mSettings.get(SettingsFile.SETTINGS_DOLPHIN).isEmpty())
{
mView.passSettingsToActivity(mSettings);
}
sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_RENDERER, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.hw_renderer, 0, true, hardwareRenderer));
sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.hw_shaders, R.string.hw_shaders_descrip, true, hardwareShader));
sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.shaders_accurate_mul, 0, false, shadersAccurateMul));
sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_GS, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.shaders_accurate_gs , 0, false, shadersAccurateGs));
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_SHADER_JIT, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.use_shader_jit, 0, true, shaderJitEnable));
sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.internal_resolution, R.string.internal_resolution_descrip, 10, "x", 0, resolutionFactor));
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.vsync , 0, false, vsyncEnable));
sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.overclock_enable, 0, false, frameLimitEnable));
sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.overclock_title, R.string.overclock_enable_description, 500, "%", 100, frameLimitValue));
// Todo: Implement ColorPickerSetting
// sl.add(new ColorPickerSetting(SettingsFile.KEY_BACKGROUND_RED,SettingsFile.KEY_BACKGROUND_GREEN,SettingsFile.KEY_BACKGROUND_BLUE, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string., R.string., Color.BLACK, backgroundColor));
/*
Check if we support stereo
If we support desktop GL then we must support at least OpenGL 3.2
If we only support OpenGLES then we need both OpenGLES 3.1 and AEP
*/
EGLHelper helper = new EGLHelper(EGLHelper.EGL_OPENGL_ES2_BIT);
if ((helper.supportsOpenGL() && helper.GetVersion() >= 320) ||
(helper.supportsGLES3() && helper.GetVersion() >= 310 && helper.SupportsExtension("GL_ANDROID_extension_pack_es31a")))
{
sl.add(new CheckBoxSetting(SettingsFile.KEY_STEREOSCOPY, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.stereoscopy, R.string.stereoscopy_descrip, false, stereoscopyEnable));
sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, SettingsFile.SECTION_RENDERER, SettingsFile.SETTINGS_DOLPHIN, R.string.sterescopy_depth,R.string.sterescopy_depth_descrip,100,"%",0, stereoscopyDepth));
}
}
private void addGcPadSubSettings(ArrayList<SettingsItem> sl, int gcPadNumber, int gcPadType)
{
/*
if (gcPadType == 1) // Emulated
{
Setting bindA = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_A + gcPadNumber);
Setting bindB = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_B + gcPadNumber);
Setting bindX = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_X + gcPadNumber);
Setting bindY = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_Y + gcPadNumber);
Setting bindZ = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_Z + gcPadNumber);
Setting bindStart = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_START + gcPadNumber);
Setting bindControlUp = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_CONTROL_UP + gcPadNumber);
Setting bindControlDown = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_CONTROL_DOWN + gcPadNumber);
Setting bindControlLeft = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_CONTROL_LEFT + gcPadNumber);
Setting bindControlRight = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_CONTROL_RIGHT + gcPadNumber);
Setting bindCUp = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_C_UP + gcPadNumber);
Setting bindCDown = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_C_DOWN + gcPadNumber);
Setting bindCLeft = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_C_LEFT + gcPadNumber);
Setting bindCRight = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_C_RIGHT + gcPadNumber);
Setting bindTriggerL = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_TRIGGER_L + gcPadNumber);
Setting bindTriggerR = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_TRIGGER_R + gcPadNumber);
Setting bindDPadUp = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_DPAD_UP + gcPadNumber);
Setting bindDPadDown = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_DPAD_DOWN + gcPadNumber);
Setting bindDPadLeft = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_DPAD_LEFT + gcPadNumber);
Setting bindDPadRight = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_BINDINGS).getSetting(SettingsFile.KEY_GCBIND_DPAD_RIGHT + gcPadNumber);
sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_A + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.button_a, bindA));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_B + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.button_b, bindB));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_X + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.button_x, bindX));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_Y + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.button_y, bindY));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_Z + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.button_z, bindZ));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_START + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.button_start, bindStart));
sl.add(new HeaderSetting(null, null, R.string.controller_control, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_CONTROL_UP + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_up, bindControlUp));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_CONTROL_DOWN + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_down, bindControlDown));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_CONTROL_LEFT + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_left, bindControlLeft));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_CONTROL_RIGHT + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_right, bindControlRight));
sl.add(new HeaderSetting(null, null, R.string.controller_c, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_C_UP + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_up, bindCUp));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_C_DOWN + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_down, bindCDown));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_C_LEFT + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_left, bindCLeft));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_C_RIGHT + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_right, bindCRight));
sl.add(new HeaderSetting(null, null, R.string.controller_trig, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_TRIGGER_L + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.trigger_left, bindTriggerL));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_TRIGGER_R + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.trigger_right, bindTriggerR));
sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_DPAD_UP + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_up, bindDPadUp));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_DPAD_DOWN + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_down, bindDPadDown));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_DPAD_LEFT + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_left, bindDPadLeft));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_DPAD_RIGHT + gcPadNumber, SettingsFile.SECTION_BINDINGS, SettingsFile.SETTINGS_DOLPHIN, R.string.generic_right, bindDPadRight));
}
else // Adapter
{
Setting rumble = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_CORE).getSetting(SettingsFile.KEY_GCADAPTER_RUMBLE + gcPadNumber);
Setting bongos = mSettings.get(SettingsFile.SETTINGS_DOLPHIN).get(SettingsFile.SECTION_CORE).getSetting(SettingsFile.KEY_GCADAPTER_BONGOS + gcPadNumber);
sl.add(new CheckBoxSetting(SettingsFile.KEY_GCADAPTER_RUMBLE + gcPadNumber, SettingsFile.SECTION_CORE, SettingsFile.SETTINGS_DOLPHIN, R.string.gc_adapter_rumble, R.string.gc_adapter_rumble_description, false, rumble));
sl.add(new CheckBoxSetting(SettingsFile.KEY_GCADAPTER_BONGOS + gcPadNumber, SettingsFile.SECTION_CORE, SettingsFile.SETTINGS_DOLPHIN, R.string.gc_adapter_bongos, R.string.gc_adapter_bongos_description, false, bongos));
}
*/
}
private boolean getInvertedBooleanValue(int file, String section, String key, boolean defaultValue)
{
try
{
return ((IntSetting) mSettings.get(file).get(section).getSetting(key)).getValue() != 1;
}
catch (NullPointerException ex)
{
return defaultValue;
}
}
}

View File

@ -0,0 +1,104 @@
package org.citra.citra_android.ui.settings;
import android.support.v4.app.FragmentActivity;
import org.citra.citra_android.model.settings.Setting;
import org.citra.citra_android.model.settings.SettingSection;
import org.citra.citra_android.model.settings.view.SettingsItem;
import java.util.ArrayList;
import java.util.HashMap;
/**
* 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(ArrayList<HashMap<String, SettingSection>> 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(ArrayList<HashMap<String, SettingSection>> 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
*/
void showToastMessage(String message);
/**
* 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();
/**
* Have the fragment tell the containing Activity that a GCPad's setting was modified.
*
* @param key Identifier for the GCPad that was modified.
* @param value New setting for the GCPad.
*/
void onGcPadSettingChanged(String key, int value);
/**
* Have the fragment tell the containing Activity that a Wiimote's setting was modified.
*
* @param section Identifier for Wiimote that was modified; Wiimotes are identified by their section,
* not their key.
* @param value New setting for the Wiimote.
*/
void onWiimoteSettingChanged(String section, int value);
/**
* Have the fragment tell the containing Activity that an extension setting was modified.
*
* @param key Identifier for the extension that was modified.
* @param value New setting for the extension.
*/
void onExtensionSettingChanged(String key, int value);
}

View File

@ -0,0 +1,56 @@
package org.citra.citra_android.ui.settings;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* FrameLayout subclass with few Properties added to simplify animations.
*/
public final class SettingsFrameLayout extends FrameLayout
{
private float mVisibleness = 1.0f;
public SettingsFrameLayout(Context context)
{
super(context);
}
public SettingsFrameLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
{
super(context, attrs, defStyleAttr, defStyleRes);
}
public float getYFraction()
{
return getY() / getHeight();
}
public void setYFraction(float yFraction)
{
final int height = getHeight();
setY((height > 0) ? (yFraction * height) : -9999);
}
public float getVisibleness()
{
return mVisibleness;
}
public void setVisibleness(float visibleness)
{
setScaleX(visibleness);
setScaleY(visibleness);
setAlpha(visibleness);
}
}

View File

@ -0,0 +1,56 @@
package org.citra.citra_android.ui.settings.viewholder;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.view.CheckBoxSetting;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.ui.settings.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());
}
mCheckbox.setChecked(mItem.isChecked());
}
@Override
public void onClick(View clicked)
{
mCheckbox.toggle();
getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked());
}
}

View File

@ -0,0 +1,49 @@
package org.citra.citra_android.ui.settings.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.view.DateTimeSetting;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.ui.settings.SettingsAdapter;
import org.citra.citra_android.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());
}
}
@Override
public void onClick(View clicked)
{
getAdapter().onDateTimeClick(mItem);
}
}

View File

@ -0,0 +1,37 @@
package org.citra.citra_android.ui.settings.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.ui.settings.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
}
}

View File

@ -0,0 +1,53 @@
package org.citra.citra_android.ui.settings.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_android.R;
import org.citra.citra_android.model.settings.view.InputBindingSetting;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.ui.settings.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());
mTextSettingDescription.setText(sharedPreferences.getString(mItem.getKey(), ""));
}
@Override
public void onClick(View clicked)
{
getAdapter().onInputBindingClick(mItem, getAdapterPosition());
}
}

View File

@ -0,0 +1,51 @@
package org.citra.citra_android.ui.settings.viewholder;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.ui.settings.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);
}

View File

@ -0,0 +1,48 @@
package org.citra.citra_android.ui.settings.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.model.settings.view.SingleChoiceSetting;
import org.citra.citra_android.ui.settings.SettingsAdapter;
public final class SingleChoiceViewHolder extends SettingViewHolder
{
private SingleChoiceSetting 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 = (SingleChoiceSetting) item;
mTextSettingName.setText(item.getNameId());
if (item.getDescriptionId() > 0)
{
mTextSettingDescription.setText(item.getDescriptionId());
}
}
@Override
public void onClick(View clicked)
{
getAdapter().onSingleChoiceClick(mItem);
}
}

View File

@ -0,0 +1,49 @@
package org.citra.citra_android.ui.settings.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.model.settings.view.SliderSetting;
import org.citra.citra_android.ui.settings.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());
}
}
@Override
public void onClick(View clicked)
{
getAdapter().onSliderClick(mItem);
}
}

View File

@ -0,0 +1,48 @@
package org.citra.citra_android.ui.settings.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_android.R;
import org.citra.citra_android.model.settings.view.SettingsItem;
import org.citra.citra_android.model.settings.view.SubmenuSetting;
import org.citra.citra_android.ui.settings.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());
}
}
@Override
public void onClick(View clicked)
{
getAdapter().onSubmenuClick(mItem);
}
}

View File

@ -0,0 +1,44 @@
package org.citra.citra_android.utils;
import android.content.AsyncQueryHandler;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import org.citra.citra_android.model.GameDatabase;
import org.citra.citra_android.model.GameProvider;
public class AddDirectoryHelper
{
private Context mContext;
public interface AddDirectoryListener
{
void onDirectoryAdded();
}
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);
}
}

View File

@ -0,0 +1,29 @@
package org.citra.citra_android.utils;
import android.view.View;
import android.view.ViewPropertyAnimator;
public final class Animations
{
private Animations()
{
}
public static ViewPropertyAnimator fadeViewIn(View view)
{
view.setVisibility(View.VISIBLE);
return view.animate()
.withLayer()
.setDuration(100)
.alpha(1.0f);
}
public static ViewPropertyAnimator fadeViewOut(View view)
{
return view.animate()
.withLayer()
.setDuration(300)
.alpha(0.0f);
}
}

View File

@ -0,0 +1,83 @@
package org.citra.citra_android.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
// Even though the triggers are L/R2, without mappings they generate L/R1 events.
return keyCode == KeyEvent.KEYCODE_BUTTON_L1 || keyCode == KeyEvent.KEYCODE_BUTTON_R1;
}
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;
}
}

View File

@ -0,0 +1,28 @@
package org.citra.citra_android.utils;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.citra.citra_android.services.DirectoryInitializationService;
import org.citra.citra_android.services.DirectoryInitializationService.DirectoryInitializationState;
import rx.functions.Action1;
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(DirectoryInitializationService.EXTRA_STATE);
callback.call(state);
}
}

View File

@ -0,0 +1,389 @@
/**
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_android.utils;
import android.opengl.GLES30;
import org.citra.citra_android.NativeLibrary;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL10;
/**
* Utility class that abstracts all the stuff about
* EGL initialization out of the way if all that is
* wanted is to query the underlying GL API for information.
*/
public final class EGLHelper
{
private final EGL10 mEGL;
private final EGLDisplay mDisplay;
private EGLConfig[] mEGLConfigs;
private EGLContext mEGLContext;
private EGLSurface mEGLSurface;
private GL10 mGL;
// GL support flags
private boolean supportGL;
private boolean supportGLES2;
private boolean supportGLES3;
// Renderable type bitmasks
public static final int EGL_OPENGL_ES_BIT = 0x0001;
public static final int EGL_OPENGL_ES2_BIT = 0x0004;
public static final int EGL_OPENGL_BIT = 0x0008;
public static final int EGL_OPENGL_ES3_BIT_KHR = 0x0040;
// API types
public static final int EGL_OPENGL_ES_API = 0x30A0;
public static final int EGL_OPENGL_API = 0x30A2;
/**
* Constructor
* <p>
* Initializes the underlying {@link EGLSurface} with a width and height of 1.
* This is useful if all you need to use this class for is to query information
* from specific API contexts.
*
* @param renderableType Bitmask indicating which types of client API contexts
* the framebuffer config must support.
*/
public EGLHelper(int renderableType)
{
this(1, 1, renderableType);
}
/**
* Constructor
*
* @param width Width of the underlying {@link EGLSurface}.
* @param height Height of the underlying {@link EGLSurface}.
* @param renderableType Bitmask indicating which types of client API contexts
* the framebuffer config must support.
*/
public EGLHelper(int width, int height, int renderableType)
{
// Initialize handle to an EGL display.
mEGL = (EGL10) EGLContext.getEGL();
mDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
// If a display is present, initialize EGL.
if (mDisplay != EGL10.EGL_NO_DISPLAY)
{
int[] version = new int[2];
if (mEGL.eglInitialize(mDisplay, version))
{
// Detect supported GL APIs, initialize configs, etc.
detect();
// Create context and surface
create(width, height, renderableType);
}
else
{
Log.error("[EGLHelper] Error initializing EGL.");
}
}
else
{
Log.error("[EGLHelper] Error initializing EGL display.");
}
}
/**
* Releases all resources associated with this helper.
* <p>
* This should be called whenever this helper is no longer needed.
*/
public void closeHelper()
{
mEGL.eglTerminate(mDisplay);
}
/**
* Gets information through EGL.<br/>
* <p>
* Index 0: Vendor <br/>
* Index 1: Version <br/>
* Index 2: Renderer <br/>
* Index 3: Extensions <br/>
*
* @return information retrieved through EGL.
*/
public String[] getEGLInfo()
{
return new String[]{
mGL.glGetString(GL10.GL_VENDOR),
mGL.glGetString(GL10.GL_VERSION),
mGL.glGetString(GL10.GL_RENDERER),
mGL.glGetString(GL10.GL_EXTENSIONS),
};
}
/**
* Whether or not this device supports OpenGL.
*
* @return true if this device supports OpenGL; false otherwise.
*/
public boolean supportsOpenGL()
{
return supportGL;
}
/**
* Whether or not this device supports OpenGL ES 2.
* <br/>
* Note that if this returns true, then OpenGL ES 1 is also supported.
*
* @return true if this device supports OpenGL ES 2; false otherwise.
*/
public boolean supportsGLES2()
{
return supportGLES2;
}
/**
* Whether or not this device supports OpenGL ES 3.
* <br/>
* Note that if this returns true, then OpenGL ES 1 and 2 are also supported.
*
* @return true if this device supports OpenGL ES 3; false otherwise.
*/
public boolean supportsGLES3()
{
return supportGLES3;
}
/**
* Gets the underlying {@link EGL10} instance.
*
* @return the underlying {@link EGL10} instance.
*/
public EGL10 getEGL()
{
return mEGL;
}
/**
* Gets the underlying {@link GL10} instance.
*
* @return the underlying {@link GL10} instance.
*/
public GL10 getGL()
{
return mGL;
}
/**
* Gets the underlying {@link EGLDisplay}.
*
* @return the underlying {@link EGLDisplay}
*/
public EGLDisplay getDisplay()
{
return mDisplay;
}
/**
* Gets all supported framebuffer configurations for this device.
*
* @return all supported framebuffer configurations for this device.
*/
public EGLConfig[] getConfigs()
{
return mEGLConfigs;
}
/**
* Gets the underlying {@link EGLContext}.
*
* @return the underlying {@link EGLContext}.
*/
public EGLContext getContext()
{
return mEGLContext;
}
/**
* Gets the underlying {@link EGLSurface}.
*
* @return the underlying {@link EGLSurface}.
*/
public EGLSurface getSurface()
{
return mEGLSurface;
}
// Detects the specific kind of GL modes that are supported
private boolean detect()
{
// Get total number of configs available.
int[] numConfigs = new int[1];
if (!mEGL.eglGetConfigs(mDisplay, null, 0, numConfigs))
{
Log.error("[EGLHelper] Error retrieving number of EGL configs available.");
return false;
}
// Now get all the configurations
mEGLConfigs = new EGLConfig[numConfigs[0]];
if (!mEGL.eglGetConfigs(mDisplay, mEGLConfigs, mEGLConfigs.length, numConfigs))
{
Log.error("[EGLHelper] Error retrieving all EGL configs.");
return false;
}
for (EGLConfig mEGLConfig : mEGLConfigs)
{
int[] attribVal = new int[1];
boolean ret =
mEGL.eglGetConfigAttrib(mDisplay, mEGLConfig, EGL10.EGL_RENDERABLE_TYPE, attribVal);
if (ret)
{
if ((attribVal[0] & EGL_OPENGL_BIT) != 0)
supportGL = true;
if ((attribVal[0] & EGL_OPENGL_ES2_BIT) != 0)
supportGLES2 = true;
if ((attribVal[0] & EGL_OPENGL_ES3_BIT_KHR) != 0)
supportGLES3 = true;
}
}
return true;
}
// Creates the context and surface.
private void create(int width, int height, int renderableType)
{
int[] attribs = {
EGL10.EGL_WIDTH, width,
EGL10.EGL_HEIGHT, height,
EGL10.EGL_NONE
};
// Initially we just assume GLES2 will be the default context.
int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
int[] ctx_attribs = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL10.EGL_NONE
};
// Determine the type of context that will be created
// and change the attribute arrays accordingly.
switch (renderableType)
{
case EGL_OPENGL_ES_BIT:
ctx_attribs[1] = 1;
break;
case EGL_OPENGL_BIT:
ctx_attribs[0] = EGL10.EGL_NONE;
break;
case EGL_OPENGL_ES3_BIT_KHR:
ctx_attribs[1] = 3;
break;
case EGL_OPENGL_ES2_BIT:
default: // Fall-back to GLES 2.
ctx_attribs[1] = 2;
break;
}
if (renderableType == EGL_OPENGL_BIT)
NativeLibrary.eglBindAPI(EGL_OPENGL_API);
else
NativeLibrary.eglBindAPI(EGL_OPENGL_ES_API);
mEGLContext =
mEGL.eglCreateContext(mDisplay, mEGLConfigs[0], EGL10.EGL_NO_CONTEXT, ctx_attribs);
mEGLSurface = mEGL.eglCreatePbufferSurface(mDisplay, mEGLConfigs[0], attribs);
mEGL.eglMakeCurrent(mDisplay, mEGLSurface, mEGLSurface, mEGLContext);
mGL = (GL10) mEGLContext.getGL();
}
/**
* Simplified call to {@link GL10#glGetString(int)}
* <p>
* Accepts the following constants:
* <ul>
* <li>GL_VENDOR - Company responsible for the GL implementation.</li>
* <li>GL_VERSION - Version or release number.</li>
* <li>GL_RENDERER - Name of the renderer</li>
* <li>GL_SHADING_LANGUAGE_VERSION - Version or release number of the shading language </li>
* </ul>
*
* @param glEnum A symbolic constant within {@link GL10}.
* @return the string information represented by {@code glEnum}.
*/
public String glGetString(int glEnum)
{
return mGL.glGetString(glEnum);
}
/**
* Simplified call to {@link GLES30#glGetStringi(int, int)}
* <p>
* Accepts the following constants:
* <ul>
* <li>GL_VENDOR - Company responsible for the GL implementation.</li>
* <li>GL_VERSION - Version or release number.</li>
* <li>GL_RENDERER - Name of the renderer</li>
* <li>GL_SHADING_LANGUAGE_VERSION - Version or release number of the shading language </li>
* <li>GL_EXTENSIONS - Extension string supported by the implementation at {@code index}.</li>
* </ul>
*
* @param glEnum A symbolic GL constant
* @param index The index of the string to return.
* @return the string information represented by {@code glEnum} and {@code index}.
*/
public String glGetStringi(int glEnum, int index)
{
return GLES30.glGetStringi(glEnum, index);
}
public boolean SupportsExtension(String extension)
{
int[] num_ext = new int[1];
GLES30.glGetIntegerv(GLES30.GL_NUM_EXTENSIONS, num_ext, 0);
for (int i = 0; i < num_ext[0]; ++i)
{
String ext = GLES30.glGetStringi(GLES30.GL_EXTENSIONS, i);
if (ext.equals(extension))
return true;
}
return false;
}
public int GetVersion()
{
int[] major = new int[1];
int[] minor = new int[1];
GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, major, 0);
GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, minor, 0);
return major[0] * 100 + minor[0] * 10;
}
/**
* Simplified call to {@link GL10#glGetIntegerv(int, int[], int)
*
* @param glEnum A symbolic GL constant.
* @return the integer information represented by {@code glEnum}.
*/
public int glGetInteger(int glEnum)
{
int[] val = new int[1];
mGL.glGetIntegerv(glEnum, val, 0);
return val[0];
}
}

View File

@ -0,0 +1,59 @@
package org.citra.citra_android.utils;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import com.nononsenseapps.filepicker.FilePickerActivity;
import com.nononsenseapps.filepicker.Utils;
import org.citra.citra_android.activities.CustomFilePickerActivity;
import org.citra.citra_android.ui.main.MainPresenter;
import java.io.File;
import java.util.List;
public final class FileBrowserHelper
{
public static void openDirectoryPicker(FragmentActivity activity)
{
Intent i = new Intent(activity, CustomFilePickerActivity.class);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
Environment.getExternalStorageDirectory().getPath());
activity.startActivityForResult(i, MainPresenter.REQUEST_ADD_DIRECTORY);
}
public static void openFilePicker(FragmentActivity activity, int requestCode)
{
Intent i = new Intent(activity, CustomFilePickerActivity.class);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
Environment.getExternalStorageDirectory().getPath());
activity.startActivityForResult(i, requestCode);
}
@Nullable
public static String getSelectedDirectory(Intent result)
{
// Use the provided utility method to parse the result
List<Uri> files = Utils.getSelectedFilesFromResult(result);
if (!files.isEmpty())
{
File file = Utils.getFileForUri(files.get(0));
return file.getAbsolutePath();
}
return null;
}
}

View File

@ -0,0 +1,30 @@
package org.citra.citra_android.utils;
import android.graphics.Bitmap;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Request;
import com.squareup.picasso.RequestHandler;
import org.citra.citra_android.NativeLibrary;
import java.nio.IntBuffer;
public class GameBannerRequestHandler extends RequestHandler
{
@Override
public boolean canHandleRequest(Request data)
{
return "iso".equals(data.uri.getScheme());
}
@Override
public Result load(Request request, int networkPolicy)
{
String url = request.uri.getHost() + request.uri.getPath();
int[] vector = NativeLibrary.GetBanner(url);
Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565);
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector));
return new Result(bitmap, Picasso.LoadedFrom.DISK);
}
}

View File

@ -0,0 +1,161 @@
package org.citra.citra_android.utils;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.hardware.usb.UsbConfiguration;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.widget.Toast;
import org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.services.USBPermService;
import java.util.HashMap;
import java.util.Map;
public class Java_GCAdapter
{
public static UsbManager manager;
static byte[] controller_payload = new byte[37];
static UsbDeviceConnection usb_con;
static UsbInterface usb_intf;
static UsbEndpoint usb_in;
static UsbEndpoint usb_out;
private static void RequestPermission()
{
Context context = NativeLibrary.sEmulationActivity.get();
if (context != null)
{
HashMap<String, UsbDevice> devices = manager.getDeviceList();
for (Map.Entry<String, UsbDevice> pair : devices.entrySet())
{
UsbDevice dev = pair.getValue();
if (dev.getProductId() == 0x0337 && dev.getVendorId() == 0x057e)
{
if (!manager.hasPermission(dev))
{
Intent intent = new Intent();
PendingIntent pend_intent;
intent.setClass(context, USBPermService.class);
pend_intent = PendingIntent.getService(context, 0, intent, 0);
manager.requestPermission(dev, pend_intent);
}
}
}
}
else
{
Log.warning("Cannot request GameCube Adapter permission as EmulationActivity is null.");
}
}
public static void Shutdown()
{
usb_con.close();
}
public static int GetFD()
{
return usb_con.getFileDescriptor();
}
public static boolean QueryAdapter()
{
HashMap<String, UsbDevice> devices = manager.getDeviceList();
for (Map.Entry<String, UsbDevice> pair : devices.entrySet())
{
UsbDevice dev = pair.getValue();
if (dev.getProductId() == 0x0337 && dev.getVendorId() == 0x057e)
{
if (manager.hasPermission(dev))
return true;
else
RequestPermission();
}
}
return false;
}
public static void InitAdapter()
{
byte[] init = {0x13};
usb_con.bulkTransfer(usb_in, init, init.length, 0);
}
public static int Input()
{
return usb_con.bulkTransfer(usb_in, controller_payload, controller_payload.length, 16);
}
public static int Output(byte[] rumble)
{
return usb_con.bulkTransfer(usb_out, rumble, 5, 16);
}
public static boolean OpenAdapter()
{
HashMap<String, UsbDevice> devices = manager.getDeviceList();
for (Map.Entry<String, UsbDevice> pair : devices.entrySet())
{
UsbDevice dev = pair.getValue();
if (dev.getProductId() == 0x0337 && dev.getVendorId() == 0x057e)
{
if (manager.hasPermission(dev))
{
usb_con = manager.openDevice(dev);
Log.info("GCAdapter: Number of configurations: " + dev.getConfigurationCount());
Log.info("GCAdapter: Number of interfaces: " + dev.getInterfaceCount());
if (dev.getConfigurationCount() > 0 && dev.getInterfaceCount() > 0)
{
UsbConfiguration conf = dev.getConfiguration(0);
usb_intf = conf.getInterface(0);
usb_con.claimInterface(usb_intf, true);
Log.info("GCAdapter: Number of endpoints: " + usb_intf.getEndpointCount());
if (usb_intf.getEndpointCount() == 2)
{
for (int i = 0; i < usb_intf.getEndpointCount(); ++i)
if (usb_intf.getEndpoint(i).getDirection() == UsbConstants.USB_DIR_IN)
usb_in = usb_intf.getEndpoint(i);
else
usb_out = usb_intf.getEndpoint(i);
InitAdapter();
return true;
}
else
{
usb_con.releaseInterface(usb_intf);
}
}
final Activity emulationActivity = NativeLibrary.sEmulationActivity.get();
if (emulationActivity != null)
{
emulationActivity.runOnUiThread(() -> Toast.makeText(emulationActivity,
"GameCube Adapter couldn't be opened. Please re-plug the device.",
Toast.LENGTH_LONG).show());
}
else
{
Log.warning("Cannot show toast for GameCube Adapter failure.");
}
usb_con.close();
}
}
}
return false;
}
}

View File

@ -0,0 +1,53 @@
package org.citra.citra_android.utils;
import org.citra.citra_android.BuildConfig;
/**
* Contains methods that call through to {@link android.util.Log}, but
* with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log
* levels in release builds.
*/
public final class Log
{
private static final String TAG = "Citra Frontend";
private Log()
{
}
public static void verbose(String message)
{
if (BuildConfig.DEBUG)
{
android.util.Log.v(TAG, message);
}
}
public static void debug(String message)
{
if (BuildConfig.DEBUG)
{
android.util.Log.d(TAG, message);
}
}
public static void info(String message)
{
android.util.Log.i(TAG, message);
}
public static void warning(String message)
{
android.util.Log.w(TAG, message);
}
public static void error(String message)
{
android.util.Log.e(TAG, message);
}
public static void wtf(String message)
{
android.util.Log.wtf(TAG, message);
}
}

View File

@ -0,0 +1,72 @@
package org.citra.citra_android.utils;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.widget.Toast;
import org.citra.citra_android.R;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
public class PermissionsHandler
{
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
@TargetApi(Build.VERSION_CODES.M)
public static boolean checkWritePermission(final FragmentActivity activity)
{
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
{
return true;
}
int hasWritePermission = ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE);
if (hasWritePermission != PackageManager.PERMISSION_GRANTED)
{
if (activity.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE))
{
showMessageOKCancel(activity, activity.getString(R.string.write_permission_needed),
(dialog, which) -> activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
REQUEST_CODE_WRITE_PERMISSION));
return false;
}
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
REQUEST_CODE_WRITE_PERMISSION);
return false;
}
return true;
}
public static boolean hasWriteAccess(Context context)
{
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
int hasWritePermission = ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE);
return hasWritePermission == PackageManager.PERMISSION_GRANTED;
}
return true;
}
private static void showMessageOKCancel(final FragmentActivity activity, String message,
DialogInterface.OnClickListener okListener)
{
new AlertDialog.Builder(activity)
.setMessage(message)
.setPositiveButton(android.R.string.ok, okListener)
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
Toast.makeText(activity, R.string.write_permission_needed, Toast.LENGTH_SHORT)
.show())
.create()
.show();
}
}

View File

@ -0,0 +1,50 @@
package org.citra.citra_android.utils;
import android.graphics.Bitmap;
import android.net.Uri;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;
import org.citra.citra_android.R;
import java.io.File;
import java.net.URI;
public class PicassoUtils
{
public static void loadGameBanner(ImageView imageView, String screenshotPath, String gamePath)
{
File file = new File(URI.create(screenshotPath.replaceAll(" ", "%20")));
if (file.exists())
{
// Fill in the view contents.
Picasso.with(imageView.getContext())
.load(screenshotPath)
.fit()
.centerCrop()
.noFade()
.noPlaceholder()
.config(Bitmap.Config.RGB_565)
.error(R.drawable.no_banner)
.into(imageView);
}
else
{
Picasso picassoInstance = new Picasso.Builder(imageView.getContext())
.addRequestHandler(new GameBannerRequestHandler())
.build();
picassoInstance
.load(Uri.parse("iso:/" + gamePath))
.fit()
.noFade()
.noPlaceholder()
.config(Bitmap.Config.RGB_565)
.error(R.drawable.no_banner)
.into(imageView);
}
}
}

View File

@ -0,0 +1,301 @@
package org.citra.citra_android.utils;
import android.support.annotation.NonNull;
import org.citra.citra_android.model.settings.FloatSetting;
import org.citra.citra_android.model.settings.IntSetting;
import org.citra.citra_android.model.settings.Setting;
import org.citra.citra_android.model.settings.SettingSection;
import org.citra.citra_android.model.settings.StringSetting;
import org.citra.citra_android.services.DirectoryInitializationService;
import org.citra.citra_android.ui.settings.SettingsActivityView;
import org.ini4j.Wini;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
/**
* A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null
* when getting a key not already in the map
*/
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);
}
}
/**
* Contains static methods for interacting with .ini files in which settings are stored.
*/
public final class SettingsFile
{
public static final int SETTINGS_DOLPHIN = 0;
public static final String FILE_NAME_CONFIG = "config";
public static final String SECTION_CONTROLS = "Controls";
public static final String SECTION_CORE = "Core";
public static final String SECTION_RENDERER = "Renderer";
public static final String SECTION_LAYOUT = "Layout";
public static final String SECTION_AUDIO = "Audio";
public static final String SECTION_SYSTEM = "System";
public static final String SECTION_CAMERA = "Camera";
public static final String SECTION_MISC = "Miscellaneous";
public static final String SECTION_DEBUGGING = "Debugging";
public static final String SECTION_WEBSERVICE = "WebService";
public static final String KEY_CPU_JIT = "use_cpu_jit";
public static final String KEY_HW_RENDERER = "use_hw_renderer";
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_SHADERS_ACCURATE_GS = "shaders_accurate_gs";
public static final String KEY_USE_SHADER_JIT = "use_shader_jit";
public static final String KEY_USE_VSYNC = "use_vsync";
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_STEREOSCOPY = "toggle_3d";
public static final String KEY_FACTOR_3D = "factor_3d";
public static final String KEY_LAYOUT_OPTION = "layout_option";
public static final String KEY_SWAP_SCREEN = "swap_screen";
public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine";
public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
public static final String KEY_VOLUME = "volume";
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_INIT_CLOCK = "init_clock";
public static final String KEY_INIT_TIME = "init_time";
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_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 SettingsFile()
{
}
/**
* 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 fileName The name of the settings file without a path or extension.
* @param view The current view.
* @return An Observable that emits a HashMap of the file's contents, then completes.
*/
public static HashMap<String, SettingSection> readFile(final String fileName,
SettingsActivityView view)
{
HashMap<String, SettingSection> sections = new SettingsSectionMap();
File ini = getSettingsFile(fileName);
BufferedReader reader = null;
try
{
reader = new BufferedReader(new FileReader(ini));
SettingSection current = null;
for (String line; (line = reader.readLine()) != null; )
{
if (line.startsWith("[") && line.endsWith("]"))
{
current = sectionFromLine(line);
sections.put(current.getName(), current);
}
else if ((current != null))
{
Setting setting = settingFromLine(current, line, fileName);
if (setting != null)
{
current.putSetting(setting);
}
}
}
}
catch (FileNotFoundException e)
{
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
view.onSettingsFileNotFound();
}
catch (IOException e)
{
Log.error("[SettingsFile] Error reading from: " + fileName + ".ini: " + e.getMessage());
view.onSettingsFileNotFound();
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (IOException e)
{
Log.error("[SettingsFile] Error closing: " + fileName + ".ini: " + e.getMessage());
}
}
}
return sections;
}
/**
* 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.
* @return An Observable representing the operation.
*/
public static void saveFile(final String fileName, final HashMap<String, SettingSection> sections,
SettingsActivityView view)
{
File ini = getSettingsFile(fileName);
Wini writer = null;
try
{
writer = new Wini(ini);
Set<String> keySet = sections.keySet();
for (String key : keySet)
{
SettingSection section = sections.get(key);
writeSection(writer, section);
}
writer.store();
}
catch (IOException e)
{
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
view.showToastMessage("Error saving " + fileName + ".ini: " + e.getMessage());
}
}
@NonNull
private static File getSettingsFile(String fileName)
{
return new File(
DirectoryInitializationService.getUserDirectory() + "/config/" + fileName + ".ini");
}
private static SettingSection sectionFromLine(String line)
{
String sectionName = line.substring(1, line.length() - 1);
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.
* @param fileName The name of the ini file the setting is in.
* @return A typed Setting containing the key/value contained in the line.
*/
private static Setting settingFromLine(SettingSection current, String line, String fileName)
{
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;
}
int file = SETTINGS_DOLPHIN;
try
{
int valueAsInt = Integer.valueOf(value);
return new IntSetting(key, current.getName(), file, valueAsInt);
}
catch (NumberFormatException ex)
{
}
try
{
float valueAsFloat = Float.valueOf(value);
return new FloatSetting(key, current.getName(), file, valueAsFloat);
}
catch (NumberFormatException ex)
{
}
return new StringSetting(key, current.getName(), file, 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());
}
}
}

View File

@ -0,0 +1,31 @@
package org.citra.citra_android.utils;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import org.citra.citra_android.activities.EmulationActivity;
public final class StartupHandler
{
public static void HandleInit(FragmentActivity parent)
{
// Ask the user to grant write permission if it's not already granted
PermissionsHandler.checkWritePermission(parent);
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();
}
}
}

View File

@ -0,0 +1,40 @@
package org.citra.citra_android.viewholders;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.citra.citra_android.R;
/**
* A simple class that stores references to views so that the GameAdapter doesn't need to
* keep calling findViewById(), which is expensive.
*/
public class GameViewHolder extends RecyclerView.ViewHolder
{
public ImageView imageScreenshot;
public TextView textGameTitle;
public TextView textCompany;
public String gameId;
// TODO Not need any of this stuff. Currently only the properties dialog needs it.
public String path;
public String title;
public String description;
public int country;
public String company;
public String screenshotPath;
public GameViewHolder(View itemView)
{
super(itemView);
itemView.setTag(this);
imageScreenshot = itemView.findViewById(R.id.image_game_screen);
textGameTitle = itemView.findViewById(R.id.text_game_title);
textCompany = itemView.findViewById(R.id.text_company);
}
}

View File

@ -0,0 +1,37 @@
package org.citra.citra_android.viewholders;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.view.View;
import android.widget.ImageView;
/**
* A simple class that stores references to views so that the GameAdapter doesn't need to
* keep calling findViewById(), which is expensive.
*/
public final class TvGameViewHolder extends Presenter.ViewHolder
{
public ImageCardView cardParent;
public ImageView imageScreenshot;
public String gameId;
// TODO Not need any of this stuff. Currently only the properties dialog needs it.
public String path;
public String title;
public String description;
public int country;
public String company;
public String screenshotPath;
public TvGameViewHolder(View itemView)
{
super(itemView);
itemView.setTag(this);
cardParent = (ImageCardView) itemView;
imageScreenshot = cardParent.getMainImageView();
}
}

View File

@ -0,0 +1,22 @@
package org.citra.citra_android.viewholders;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.view.View;
public final class TvSettingsViewHolder extends Presenter.ViewHolder
{
public ImageCardView cardParent;
// Determines what action to take when this item is clicked.
public int itemId;
public TvSettingsViewHolder(View itemView)
{
super(itemView);
itemView.setTag(this);
cardParent = (ImageCardView) itemView;
}
}

View File

@ -1,13 +0,0 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra_emu.citra;
import android.app.Application;
public class CitraApplication extends Application {
static {
System.loadLibrary("citra-android");
}
}

View File

@ -1,41 +0,0 @@
package org.citra_emu.citra;
public class LOG {
private interface LOG_LEVEL {
int TRACE = 0, DEBUG = 1, INFO = 2, WARNING = 3, ERROR = 4, CRITICAL = 5;
}
public static void TRACE(String msg, Object... args) {
LOG(LOG_LEVEL.TRACE, msg, args);
}
public static void DEBUG(String msg, Object... args) {
LOG(LOG_LEVEL.DEBUG, msg, args);
}
public static void INFO(String msg, Object... args) {
LOG(LOG_LEVEL.INFO, msg, args);
}
public static void WARNING(String msg, Object... args) {
LOG(LOG_LEVEL.WARNING, msg, args);
}
public static void ERROR(String msg, Object... args) {
LOG(LOG_LEVEL.ERROR, msg, args);
}
public static void CRITICAL(String msg, Object... args) {
LOG(LOG_LEVEL.CRITICAL, msg, args);
}
private static void LOG(int level, String msg, Object... args) {
StackTraceElement trace = Thread.currentThread().getStackTrace()[4];
logEntry(level, trace.getFileName(), trace.getLineNumber(), trace.getMethodName(),
String.format(msg, args));
}
private static native void logEntry(int level, String file_name, int line_number,
String function, String message);
}

View File

@ -1,58 +0,0 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra_emu.citra.ui.main;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import org.citra_emu.citra.R;
import org.citra_emu.citra.utils.FileUtil;
import org.citra_emu.citra.utils.PermissionUtil;
public final class MainActivity extends AppCompatActivity {
// Java enums suck
private interface PermissionCodes { int INITIALIZE = 0; }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PermissionUtil.verifyPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE,
PermissionCodes.INITIALIZE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case PermissionCodes.INITIALIZE:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initUserPath(FileUtil.getUserPath().toString());
initLogging();
} else {
AlertDialog.Builder dialog =
new AlertDialog.Builder(this)
.setTitle("Permission Error")
.setMessage("Citra requires storage permissions to function.")
.setCancelable(false)
.setPositiveButton("OK", (dialogInterface, which) -> {
PermissionUtil.verifyPermission(
MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE,
PermissionCodes.INITIALIZE);
});
dialog.show();
}
}
}
private static native void initUserPath(String path);
private static native void initLogging();
}

View File

@ -1,19 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra_emu.citra.utils;
import android.os.Environment;
import java.io.File;
public class FileUtil {
public static File getUserPath() {
File storage = Environment.getExternalStorageDirectory();
File userPath = new File(storage, "citra");
if (!userPath.isDirectory())
userPath.mkdir();
return userPath;
}
}

View File

@ -1,32 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra_emu.citra.utils;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
public class PermissionUtil {
/**
* Checks a permission, if needed shows a dialog to request it
*
* @param activity the activity requiring the permission
* @param permission the permission needed
* @param requestCode supplied to the callback to determine the next action
*/
public static void verifyPermission(Activity activity, String permission, int requestCode) {
if (ContextCompat.checkSelfPermission(activity, permission) ==
PackageManager.PERMISSION_GRANTED) {
// call the callback called by requestPermissions
activity.onRequestPermissionsResult(requestCode, new String[] {permission},
new int[] {PackageManager.PERMISSION_GRANTED});
return;
}
ActivityCompat.requestPermissions(activity, new String[] {permission}, requestCode);
}
}

View File

@ -9,12 +9,12 @@
#include <tuple>
#include <unordered_map>
#include <utility>
#include "citra_android/jni/button_manager.h"
#include "common/logging/log.h"
#include "common/math_util.h"
#include "common/param_package.h"
#include "input_common/main.h"
#include "input_common/sdl/sdl.h"
#include "jni/button_manager.h"
namespace InputManager {

Some files were not shown because too many files have changed in this diff Show More