Merge branch 'amiibo' into 'master'
android: Add Amiibo file support See merge request CitraInternal/citra-android!34
This commit is contained in:
parent
d3ea35f528
commit
72f5d4e664
@ -441,6 +441,10 @@ public final class NativeLibrary {
|
||||
/// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
||||
public static native void ReloadCameraDevices();
|
||||
|
||||
public static native boolean LoadAmiibo(byte[] bytes);
|
||||
|
||||
public static native void RemoveAmiibo();
|
||||
|
||||
/**
|
||||
* Button type for use in onTouchEvent
|
||||
*/
|
||||
|
@ -14,6 +14,7 @@ import java.io.File;
|
||||
|
||||
public class CustomFilePickerActivity extends FilePickerActivity {
|
||||
public static final String EXTRA_TITLE = "filepicker.intent.TITLE";
|
||||
public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS";
|
||||
|
||||
@Override
|
||||
protected AbstractFilePickerFragment<File> getFragment(
|
||||
@ -29,6 +30,9 @@ public class CustomFilePickerActivity extends FilePickerActivity {
|
||||
Intent intent = getIntent();
|
||||
int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0);
|
||||
fragment.setTitle(title);
|
||||
String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS);
|
||||
fragment.setAllowedExtensions(allowedExtensions);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
}
|
||||
|
@ -34,10 +34,16 @@ import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.camera.StillImageCameraHelper;
|
||||
import org.citra.citra_emu.fragments.EmulationFragment;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
import org.citra.citra_emu.utils.ControllerMappingHelper;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
@ -58,7 +64,10 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
public static final int MENU_ACTION_RESET_OVERLAY = 10;
|
||||
public static final int MENU_ACTION_SHOW_OVERLAY = 11;
|
||||
public static final int MENU_ACTION_OPEN_SETTINGS = 12;
|
||||
public static final int MENU_ACTION_LOAD_AMIIBO = 13;
|
||||
public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
|
||||
|
||||
public static final int REQUEST_SELECT_AMIIBO = 2;
|
||||
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
||||
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
|
||||
|
||||
@ -87,6 +96,10 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
.append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO);
|
||||
}
|
||||
|
||||
private View mDecorView;
|
||||
@ -375,6 +388,14 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
case MENU_ACTION_OPEN_SETTINGS:
|
||||
SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
|
||||
break;
|
||||
|
||||
case MENU_ACTION_LOAD_AMIIBO:
|
||||
FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO, R.string.select_amiibo, Collections.singletonList("bin"));
|
||||
break;
|
||||
|
||||
case MENU_ACTION_REMOVE_AMIIBO:
|
||||
RemoveAmiibo();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -422,6 +443,47 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
|
||||
super.onActivityResult(requestCode, resultCode, result);
|
||||
switch (requestCode) {
|
||||
case REQUEST_SELECT_AMIIBO:
|
||||
// If the user picked a file, as opposed to just backing out.
|
||||
if (resultCode == MainActivity.RESULT_OK) {
|
||||
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(result);
|
||||
if (selectedFiles == null)
|
||||
return;
|
||||
|
||||
onAmiiboSelected(selectedFiles[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onAmiiboSelected(String selectedFile) {
|
||||
File file = new File(selectedFile);
|
||||
boolean success = false;
|
||||
try {
|
||||
byte[] bytes = FileUtil.getBytesFromFile(file);
|
||||
success = NativeLibrary.LoadAmiibo(bytes);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.amiibo_load_error)
|
||||
.setMessage(R.string.amiibo_load_error_message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAmiibo() {
|
||||
NativeLibrary.RemoveAmiibo();
|
||||
}
|
||||
|
||||
private void toggleControls() {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
boolean[] enabledButtons = new boolean[14];
|
||||
|
@ -17,12 +17,13 @@ import org.citra.citra_emu.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CustomFilePickerFragment extends FilePickerFragment {
|
||||
private static String ALL_FILES = "*";
|
||||
private int mTitle;
|
||||
private static final Set<String> extensions = new HashSet<>(Arrays.asList("elf", "axf", "cci", "3ds", "cxi", "app", "3dsx", "cia"));
|
||||
private static List<String> extensions = Collections.singletonList(ALL_FILES);
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@ -66,6 +67,12 @@ public class CustomFilePickerFragment extends FilePickerFragment {
|
||||
mTitle = title;
|
||||
}
|
||||
|
||||
public void setAllowedExtensions(String allowedExtensions) {
|
||||
if (allowedExtensions == null)
|
||||
return;
|
||||
|
||||
extensions = Arrays.asList(allowedExtensions.split(","));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isItemVisible(@NonNull final File file) {
|
||||
@ -73,7 +80,7 @@ public class CustomFilePickerFragment extends FilePickerFragment {
|
||||
// files if the files don't show up in the file picker when mode == MODE_DIR.
|
||||
// To avoid this, show files even when the user needs to select a directory.
|
||||
return (showHiddenItems || !file.isHidden()) &&
|
||||
(file.isDirectory() ||
|
||||
(file.isDirectory() || extensions.contains(ALL_FILES) ||
|
||||
extensions.contains(fileExtension(file.getName()).toLowerCase()));
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ package org.citra.citra_emu.model;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Environment;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
|
@ -26,6 +26,8 @@ import org.citra.citra_emu.utils.PicassoUtils;
|
||||
import org.citra.citra_emu.utils.StartupHandler;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -127,7 +129,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||
public void launchFileListActivity() {
|
||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
||||
FileBrowserHelper.openDirectoryPicker(this, MainPresenter.REQUEST_ADD_DIRECTORY,
|
||||
R.string.select_game_folder);
|
||||
R.string.select_game_folder, Arrays.asList("elf", "axf", "cci", "3ds", "cxi", "app", "3dsx", "cia"));
|
||||
} else {
|
||||
PermissionsHandler.checkWritePermission(this);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
public final class FileBrowserHelper {
|
||||
public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title) {
|
||||
public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) {
|
||||
Intent i = new Intent(activity, CustomFilePickerActivity.class);
|
||||
|
||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
|
||||
@ -25,11 +25,12 @@ public final class FileBrowserHelper {
|
||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
|
||||
Environment.getExternalStorageDirectory().getPath());
|
||||
i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
|
||||
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
|
||||
|
||||
activity.startActivityForResult(i, requestCode);
|
||||
}
|
||||
|
||||
public static void openFilePicker(FragmentActivity activity, int requestCode, int title) {
|
||||
public static void openFilePicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) {
|
||||
Intent i = new Intent(activity, CustomFilePickerActivity.class);
|
||||
|
||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
|
||||
@ -38,6 +39,7 @@ public final class FileBrowserHelper {
|
||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
|
||||
Environment.getExternalStorageDirectory().getPath());
|
||||
i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
|
||||
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
|
||||
|
||||
activity.startActivityForResult(i, requestCode);
|
||||
}
|
||||
@ -53,4 +55,18 @@ public final class FileBrowserHelper {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String[] getSelectedFiles(Intent result) {
|
||||
// Use the provided utility method to parse the result
|
||||
List<Uri> files = Utils.getSelectedFilesFromResult(result);
|
||||
if (!files.isEmpty()) {
|
||||
String[] paths = new String[files.size()];
|
||||
for (int i = 0; i < files.size(); i++)
|
||||
paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath();
|
||||
return paths;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class FileUtil {
|
||||
public static byte[] getBytesFromFile(File file) throws IOException {
|
||||
final long length = file.length();
|
||||
|
||||
// You cannot create an array using a long type.
|
||||
if (length > Integer.MAX_VALUE) {
|
||||
// File is too large
|
||||
throw new IOException("File is too large!");
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[(int) length];
|
||||
|
||||
int offset = 0;
|
||||
int numRead;
|
||||
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
while (offset < bytes.length
|
||||
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
|
||||
offset += numRead;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all the bytes have been read in
|
||||
if (offset < bytes.length) {
|
||||
throw new IOException("Could not completely read file " + file.getName());
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@
|
||||
#include "core/frontend/camera/factory.h"
|
||||
#include "core/frontend/scope_acquire_context.h"
|
||||
#include "core/hle/service/am/am.h"
|
||||
#include "core/hle/service/nfc/nfc.h"
|
||||
#include "core/settings.h"
|
||||
#include "jni/applets/swkbd.h"
|
||||
#include "jni/button_manager.h"
|
||||
@ -481,4 +482,30 @@ void Java_org_citra_citra_1emu_NativeLibrary_ReloadCameraDevices(JNIEnv* env, jc
|
||||
}
|
||||
}
|
||||
|
||||
jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv *env, jclass clazz, jbyteArray bytes) {
|
||||
Core::System& system{Core::System::GetInstance()};
|
||||
Service::SM::ServiceManager& sm = system.ServiceManager();
|
||||
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
|
||||
if (nfc == nullptr || env->GetArrayLength(bytes) != sizeof(Service::NFC::AmiiboData)) {
|
||||
return static_cast<jboolean>(false);
|
||||
}
|
||||
|
||||
Service::NFC::AmiiboData amiibo_data{};
|
||||
env->GetByteArrayRegion(bytes, 0, sizeof(Service::NFC::AmiiboData), reinterpret_cast<jbyte*>(&amiibo_data));
|
||||
|
||||
nfc->LoadAmiibo(amiibo_data);
|
||||
return static_cast<jboolean>(true);
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv *env, jclass clazz) {
|
||||
Core::System &system{Core::System::GetInstance()};
|
||||
Service::SM::ServiceManager &sm = system.ServiceManager();
|
||||
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
|
||||
if (nfc == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
nfc->RemoveAmiibo();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
@ -134,6 +134,10 @@ Java_org_citra_citra_1emu_NativeLibrary_GetTextureFilterNames(JNIEnv* env, jclas
|
||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_ReloadCameraDevices(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv *env, jclass clazz, jbyteArray bytes);
|
||||
|
||||
JNIEXPORT void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv *env, jclass clazz);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
@ -25,6 +25,20 @@
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_amiibo"
|
||||
android:title="@string/menu_emulation_amiibo">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/menu_emulation_amiibo_load"
|
||||
android:title="@string/menu_emulation_amiibo_load"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_amiibo_remove"
|
||||
android:title="@string/menu_emulation_amiibo_remove"/>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_emulation_switch_screen_layout"
|
||||
app:showAsAction="never"
|
||||
|
@ -145,6 +145,12 @@
|
||||
<string name="emulation_show_overlay">Show Overlay</string>
|
||||
<string name="emulation_close_game">Close Game</string>
|
||||
<string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string>
|
||||
<string name="menu_emulation_amiibo">Amiibo</string>
|
||||
<string name="menu_emulation_amiibo_load">Load</string>
|
||||
<string name="menu_emulation_amiibo_remove">Remove</string>
|
||||
<string name="select_amiibo">Select Amiibo file</string>
|
||||
<string name="amiibo_load_error">Error loading Amiibo</string>
|
||||
<string name="amiibo_load_error_message">While loading the specified Amiibo file, an error occurred. Please check that the file is correct.</string>
|
||||
|
||||
<string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
|
||||
<string name="load_settings">Loading Settings...</string>
|
||||
|
@ -29,6 +29,7 @@
|
||||
<style name="CitraEmulationBase" parent="Theme.AppCompat.DayNight.DarkActionBar">
|
||||
<item name="colorPrimary">@color/citra_orange</item>
|
||||
<item name="colorPrimaryDark">@color/citra_orange_dark</item>
|
||||
<item name="colorAccent">@color/citra_accent</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
|
Loading…
x
Reference in New Issue
Block a user