diff --git a/src/pandroid/app/build.gradle.kts b/src/pandroid/app/build.gradle.kts
index 276eb552..f1feaf0d 100644
--- a/src/pandroid/app/build.gradle.kts
+++ b/src/pandroid/app/build.gradle.kts
@@ -38,5 +38,7 @@ android {
dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
+ implementation("androidx.preference:preference:1.2.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("com.google.code.gson:gson:2.10.1")
}
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/AndroidManifest.xml b/src/pandroid/app/src/main/AndroidManifest.xml
index 15bf6270..8caf9bb0 100644
--- a/src/pandroid/app/src/main/AndroidManifest.xml
+++ b/src/pandroid/app/src/main/AndroidManifest.xml
@@ -24,7 +24,8 @@
tools:targetApi="31">
+ android:exported="true"
+ android:configChanges="orientation">
@@ -34,5 +35,11 @@
android:name=".app.GameActivity"
android:configChanges="screenSize|screenLayout|orientation|density|uiMode">
+
+
+
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java
index 2da73b97..286a7ed3 100644
--- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java
@@ -2,6 +2,8 @@ package com.panda3ds.pandroid.app;
import android.content.Intent;
import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -12,11 +14,17 @@ import android.widget.Toast;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.app.game.AlberInputListener;
+import com.panda3ds.pandroid.input.InputHandler;
+import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.view.PandaGlSurfaceView;
import com.panda3ds.pandroid.view.PandaLayoutController;
public class GameActivity extends BaseActivity {
+
+ private final AlberInputListener inputListener = new AlberInputListener();
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -33,7 +41,7 @@ public class GameActivity extends BaseActivity {
setContentView(R.layout.game_activity);
((FrameLayout) findViewById(R.id.panda_gl_frame))
- .addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ .addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
PandaLayoutController controllerLayout = findViewById(R.id.controller_layout);
controllerLayout.initialize();
@@ -46,5 +54,30 @@ public class GameActivity extends BaseActivity {
super.onResume();
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ InputHandler.reset();
+ InputHandler.setMotionDeadZone(InputMap.getDeadZone());
+ InputHandler.setEventListener(inputListener);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ InputHandler.reset();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (InputHandler.processKeyEvent(event))
+ return true;
+
+ return super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+ if (InputHandler.processMotionEvent(ev))
+ return true;
+
+ return super.dispatchGenericMotionEvent(ev);
}
}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java
index f4fc27bf..29070a88 100644
--- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java
@@ -8,17 +8,27 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
-import android.widget.Toast;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import com.panda3ds.pandroid.R;
-import com.panda3ds.pandroid.utils.Constants;
-import com.panda3ds.pandroid.utils.PathUtils;
+import android.view.MenuItem;
-public class MainActivity extends BaseActivity {
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.google.android.material.navigation.NavigationBarView;
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.app.main.GamesFragment;
+import com.panda3ds.pandroid.app.main.SearchFragment;
+import com.panda3ds.pandroid.app.main.SettingsFragment;
+
+public class MainActivity extends BaseActivity implements NavigationBarView.OnItemSelectedListener {
private static final int PICK_ROM = 2;
private static final int PERMISSION_REQUEST_CODE = 3;
+ private final GamesFragment gamesFragment = new GamesFragment();
+ private final SearchFragment searchFragment = new SearchFragment();
+ private final SettingsFragment settingsFragment = new SettingsFragment();
+
private void openFile() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
@@ -35,23 +45,35 @@ public class MainActivity extends BaseActivity {
startActivity(intent);
}
} else {
- ActivityCompat.requestPermissions(this, new String[] {READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
- ActivityCompat.requestPermissions(this, new String[] {WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
+ ActivityCompat.requestPermissions(this, new String[]{READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
+ ActivityCompat.requestPermissions(this, new String[]{WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
}
setContentView(R.layout.activity_main);
- findViewById(R.id.load_rom).setOnClickListener(v -> { openFile(); });
+
+ NavigationBarView bar = findViewById(R.id.navigation);
+ bar.setOnItemSelectedListener(this);
+ bar.postDelayed(() -> bar.setSelectedItemId(bar.getSelectedItemId()), 5);
}
@Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == PICK_ROM) {
- if (resultCode == RESULT_OK) {
- String path = PathUtils.getPath(getApplicationContext(), data.getData());
- Toast.makeText(getApplicationContext(), "pandroid opening " + path, Toast.LENGTH_LONG).show();
- startActivity(new Intent(this, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path));
- }
- super.onActivityResult(requestCode, resultCode, data);
+ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+ int id = item.getItemId();
+ FragmentManager manager = getSupportFragmentManager();
+ Fragment fragment;
+ if (id == R.id.games) {
+ fragment = gamesFragment;
+ } else if (id == R.id.search) {
+ fragment = searchFragment;
+ } else if (id == R.id.settings) {
+ fragment = settingsFragment;
+ } else {
+ return false;
}
+
+ manager.beginTransaction()
+ .replace(R.id.fragment_container, fragment)
+ .commitNow();
+ return true;
}
}
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java
index 0e284db6..246decec 100644
--- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java
@@ -4,6 +4,8 @@ import android.app.Application;
import android.content.Context;
import com.panda3ds.pandroid.data.config.GlobalConfig;
+import com.panda3ds.pandroid.input.InputMap;
+import com.panda3ds.pandroid.utils.GameUtils;
public class PandroidApplication extends Application {
private static Context appContext;
@@ -13,6 +15,8 @@ public class PandroidApplication extends Application {
super.onCreate();
appContext = this;
GlobalConfig.initialize();
+ GameUtils.initialize();
+ InputMap.initialize();
}
public static Context getAppContext() {
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java
new file mode 100644
index 00000000..3b8ebb2e
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java
@@ -0,0 +1,54 @@
+package com.panda3ds.pandroid.app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.utils.Constants;
+
+public class PreferenceActivity extends BaseActivity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+
+ setContentView(R.layout.activity_preference);
+ setSupportActionBar(findViewById(R.id.toolbar));
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ if (!intent.hasExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT)) {
+ finish();
+ return;
+ }
+
+ try {
+ Class> clazz = getClassLoader().loadClass(intent.getStringExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT));
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, (Fragment) clazz.newInstance())
+ .commitNow();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void launch(Context context, Class extends Fragment> clazz) {
+ context.startActivity(new Intent(context, PreferenceActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT, clazz.getName()));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == android.R.id.home)
+ finish();
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java
new file mode 100644
index 00000000..f459aa6d
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java
@@ -0,0 +1,19 @@
+package com.panda3ds.pandroid.app.base;
+
+import android.annotation.SuppressLint;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.panda3ds.pandroid.lang.Function;
+
+public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
+ @SuppressLint("RestrictedApi")
+ protected void setItemClick(String key, Function listener){
+ findPreference(key).setOnPreferenceClickListener(preference -> {
+ listener.run(preference);
+ getPreferenceScreen().performClick();
+ return false;
+ });
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java
new file mode 100644
index 00000000..c500d970
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java
@@ -0,0 +1,54 @@
+package com.panda3ds.pandroid.app.game;
+
+import com.panda3ds.pandroid.AlberDriver;
+import com.panda3ds.pandroid.input.InputEvent;
+import com.panda3ds.pandroid.input.InputMap;
+import com.panda3ds.pandroid.input.KeyName;
+import com.panda3ds.pandroid.lang.Function;
+import com.panda3ds.pandroid.math.Vector2;
+
+public class AlberInputListener implements Function {
+
+ private final Vector2 axis = new Vector2(0.0f, 0.0f);
+
+ @Override
+ public void run(InputEvent event) {
+ KeyName key = InputMap.relative(event.getName());
+
+ if (key == KeyName.NULL)
+ return;
+
+ boolean axisChanged = false;
+
+ switch (key) {
+ case AXIS_UP:
+ axis.y = event.getValue();
+ axisChanged = true;
+ break;
+ case AXIS_DOWN:
+ axis.y = -event.getValue();
+ axisChanged = true;
+ break;
+ case AXIS_LEFT:
+ axis.x = -event.getValue();
+ axisChanged = true;
+ break;
+ case AXIS_RIGHT:
+ axis.x = event.getValue();
+ axisChanged = true;
+ break;
+ default:
+ if (event.isDown()) {
+ AlberDriver.KeyDown(key.getKeyId());
+ } else {
+ AlberDriver.KeyUp(key.getKeyId());
+ }
+ break;
+ }
+
+ if (axisChanged) {
+ AlberDriver.SetCirclepadAxis(Math.round(axis.x * 0x9C), Math.round(axis.y * 0x9C));
+ }
+ }
+
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java
new file mode 100644
index 00000000..ef49f6f1
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java
@@ -0,0 +1,75 @@
+package com.panda3ds.pandroid.app.main;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.data.game.GameMetadata;
+import com.panda3ds.pandroid.utils.FileUtils;
+import com.panda3ds.pandroid.utils.GameUtils;
+import com.panda3ds.pandroid.view.gamesgrid.GamesGridView;
+
+public class GamesFragment extends Fragment implements ActivityResultCallback {
+
+ private final ActivityResultContracts.OpenDocument openRomContract = new ActivityResultContracts.OpenDocument();
+ private ActivityResultLauncher pickFileRequest;
+ private GamesGridView gameListView;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_games, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ gameListView = view.findViewById(R.id.games);
+
+ view.findViewById(R.id.add_rom).setOnClickListener((v) -> pickFileRequest.launch(new String[]{"*/*"}));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ gameListView.setGameList(GameUtils.getGames());
+ }
+
+ @Override
+ public void onActivityResult(Uri result) {
+ if (result != null) {
+ String uri = result.toString();
+ if (GameUtils.findByRomPath(uri) == null) {
+ FileUtils.makeUriPermanent(uri, FileUtils.MODE_READ);
+ GameMetadata game = new GameMetadata(FileUtils.getName(uri).split("\\.")[0], uri, "Unknown");
+ GameUtils.addGame(game);
+ GameUtils.launch(requireActivity(), game);
+ }
+ }
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ pickFileRequest = registerForActivityResult(openRomContract, this);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (pickFileRequest != null) {
+ pickFileRequest.unregister();
+ pickFileRequest = null;
+ }
+ super.onDestroy();
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java
new file mode 100644
index 00000000..213d55fc
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java
@@ -0,0 +1,67 @@
+package com.panda3ds.pandroid.app.main;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatEditText;
+import androidx.fragment.app.Fragment;
+
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.data.game.GameMetadata;
+import com.panda3ds.pandroid.utils.GameUtils;
+import com.panda3ds.pandroid.utils.SearchAgent;
+import com.panda3ds.pandroid.view.SimpleTextWatcher;
+import com.panda3ds.pandroid.view.gamesgrid.GamesGridView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchFragment extends Fragment {
+
+ private final SearchAgent searchAgent = new SearchAgent();
+ private GamesGridView gamesListView;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_search, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ gamesListView = view.findViewById(R.id.games);
+
+ ((AppCompatEditText) view.findViewById(R.id.search_bar))
+ .addTextChangedListener((SimpleTextWatcher) this::search);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ searchAgent.clearBuffer();
+ for (GameMetadata game : GameUtils.getGames()) {
+ searchAgent.addToBuffer(game.getId(), game.getTitle(), game.getPublisher());
+ }
+ search("");
+ }
+
+ private void search(String query) {
+ List resultIds = searchAgent.search(query);
+ ArrayList games = new ArrayList<>(GameUtils.getGames());
+ Object[] resultObj = games.stream()
+ .filter(gameMetadata -> resultIds.contains(gameMetadata.getId()))
+ .toArray();
+
+ games.clear();
+ for (Object res : resultObj)
+ games.add((GameMetadata) res);
+
+ gamesListView.setGameList(games);
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java
new file mode 100644
index 00000000..22f888fe
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java
@@ -0,0 +1,18 @@
+package com.panda3ds.pandroid.app.main;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.app.PreferenceActivity;
+import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
+import com.panda3ds.pandroid.app.preferences.InputMapPreferences;
+
+public class SettingsFragment extends BasePreferenceFragment {
+ @Override
+ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+ setPreferencesFromResource(R.xml.start_preferences, rootKey);
+ setItemClick("inputMap", (item) -> PreferenceActivity.launch(requireContext(), InputMapPreferences.class));
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapActivity.java
new file mode 100644
index 00000000..b7a2cbb4
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapActivity.java
@@ -0,0 +1,76 @@
+package com.panda3ds.pandroid.app.preferences;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.widget.Toast;
+
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.app.BaseActivity;
+import com.panda3ds.pandroid.input.InputEvent;
+import com.panda3ds.pandroid.input.InputHandler;
+
+import java.util.Objects;
+
+public class InputMapActivity extends BaseActivity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_input_map);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ InputHandler.reset();
+ InputHandler.setMotionDeadZone(0.8F);
+ InputHandler.setEventListener(this::onInputEvent);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ InputHandler.reset();
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+ return InputHandler.processMotionEvent(ev);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return InputHandler.processKeyEvent(event);
+ }
+
+ private void onInputEvent(InputEvent event) {
+ if (Objects.equals(event.getName(), "KEYCODE_BACK")) {
+ onBackPressed();
+ return;
+ }
+ setResult(RESULT_OK, new Intent(event.getName()));
+ Toast.makeText(this, event.getName(), Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+
+ public static final class Contract extends ActivityResultContract {
+
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull Context context, String s) {
+ return new Intent(context, InputMapActivity.class);
+ }
+
+ @Override
+ public String parseResult(int i, @Nullable Intent intent) {
+ return i == RESULT_OK ? intent.getAction() : null;
+ }
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java
new file mode 100644
index 00000000..007920bd
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java
@@ -0,0 +1,72 @@
+package com.panda3ds.pandroid.app.preferences;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
+import com.panda3ds.pandroid.input.InputMap;
+import com.panda3ds.pandroid.input.KeyName;
+
+public class InputMapPreferences extends BasePreferenceFragment implements ActivityResultCallback {
+
+ private ActivityResultLauncher requestKey;
+ private String currentKey;
+
+ @Override
+ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+ setPreferencesFromResource(R.xml.input_map_preferences, rootKey);
+ for (KeyName key : KeyName.values()) {
+ if (key == KeyName.NULL) return;
+ setItemClick(key.name(), this::onItemPressed);
+ }
+ refreshList();
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ requestKey = registerForActivityResult(new InputMapActivity.Contract(), this);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ if (requestKey != null) {
+ requestKey.unregister();
+ requestKey = null;
+ }
+ }
+
+ private void onItemPressed(Preference pref) {
+ currentKey = pref.getKey();
+ requestKey.launch(null);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshList();
+ }
+
+ private void refreshList() {
+ for (KeyName key : KeyName.values()) {
+ if (key == KeyName.NULL) continue;
+ findPreference(key.name()).setSummary(InputMap.relative(key));
+ }
+ }
+
+ @Override
+ public void onActivityResult(String result) {
+ if (result != null) {
+ InputMap.set(KeyName.valueOf(currentKey), result);
+ refreshList();
+ }
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java
new file mode 100644
index 00000000..46407302
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java
@@ -0,0 +1,44 @@
+package com.panda3ds.pandroid.data.game;
+
+import java.util.UUID;
+
+public class GameMetadata {
+
+ private final String id;
+ private final String romPath;
+ private final String title;
+ private final int[] icon = new int[48 * 48];
+ private final String publisher;
+ private final GameRegion[] regions = new GameRegion[]{GameRegion.None};
+
+ public GameMetadata(String title, String romPath, String publisher) {
+ this.id = UUID.randomUUID().toString();
+ this.title = title;
+ this.publisher = publisher;
+ this.romPath = romPath;
+ }
+
+ public String getRomPath() {
+ return romPath;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getPublisher() {
+ return publisher;
+ }
+
+ public int[] getIcon() {
+ return icon;
+ }
+
+ public GameRegion[] getRegions() {
+ return regions;
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java
new file mode 100644
index 00000000..9b99b095
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java
@@ -0,0 +1,12 @@
+package com.panda3ds.pandroid.data.game;
+
+public enum GameRegion {
+ NorthAmerican,
+ Japan,
+ Europe,
+ Australia,
+ China,
+ Korean,
+ Taiwan,
+ None
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputEvent.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputEvent.java
new file mode 100644
index 00000000..7869e00a
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputEvent.java
@@ -0,0 +1,23 @@
+package com.panda3ds.pandroid.input;
+
+public class InputEvent {
+ private final String name;
+ private final float value;
+
+ public InputEvent(String name, float value) {
+ this.name = name;
+ this.value = Math.max(0.0f, Math.min(1.0f, value));
+ }
+
+ public boolean isDown() {
+ return value > 0.0f;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public float getValue() {
+ return value;
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java
new file mode 100644
index 00000000..08142d06
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java
@@ -0,0 +1,116 @@
+package com.panda3ds.pandroid.input;
+
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.panda3ds.pandroid.lang.Function;
+
+import java.util.HashMap;
+
+public class InputHandler {
+ private static Function eventListener;
+ private static float motionDeadZone = 0.0f;
+
+ private static final int[] gamepadSources = {
+ InputDevice.SOURCE_GAMEPAD,
+ InputDevice.SOURCE_JOYSTICK
+ };
+
+ private static final int[] validSources = {
+ InputDevice.SOURCE_GAMEPAD,
+ InputDevice.SOURCE_JOYSTICK,
+ InputDevice.SOURCE_DPAD,
+ InputDevice.SOURCE_KEYBOARD
+ };
+
+ private static final HashMap motionDownEvents = new HashMap<>();
+
+ private static boolean containsSource(int[] sources, int sourceMasked) {
+ for (int source : sources) {
+ if ((sourceMasked & source) == source)
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean isGamepadSource(int sourceMask) {
+ return containsSource(gamepadSources, sourceMask);
+ }
+
+ private static boolean isSourceValid(int sourceMasked) {
+ return containsSource(validSources, sourceMasked);
+ }
+
+ public static void setEventListener(Function eventListener) {
+ InputHandler.eventListener = eventListener;
+ }
+
+ private static void handleEvent(InputEvent event) {
+ if (eventListener != null) {
+ eventListener.run(event);
+ }
+ }
+
+ public static void setMotionDeadZone(float motionDeadZone) {
+ InputHandler.motionDeadZone = motionDeadZone;
+ }
+
+ public static boolean processMotionEvent(MotionEvent event) {
+ if (!isSourceValid(event.getSource()))
+ return false;
+
+ if (isGamepadSource(event.getSource())) {
+ for (InputDevice.MotionRange range : event.getDevice().getMotionRanges()) {
+ float axisValue = event.getAxisValue(range.getAxis());
+ float value = Math.abs(axisValue);
+ String name = (MotionEvent.axisToString(range.getAxis()) + (axisValue >= 0 ? "+" : "-")).toUpperCase();
+ String reverseName = (MotionEvent.axisToString(range.getAxis()) + (axisValue >= 0 ? "-" : "+")).toUpperCase();
+
+ if (motionDownEvents.containsKey(reverseName)) {
+ motionDownEvents.remove(reverseName);
+ handleEvent(new InputEvent(reverseName.toUpperCase(), 0.0f));
+ }
+
+ if (value > motionDeadZone) {
+ motionDownEvents.put(name, value);
+ handleEvent(new InputEvent(name.toUpperCase(), (value - motionDeadZone) / (1.0f - motionDeadZone)));
+ } else if (motionDownEvents.containsKey(name)) {
+ motionDownEvents.remove(name);
+ handleEvent(new InputEvent(name.toUpperCase(), 0.0f));
+ }
+
+ }
+ }
+
+ return true;
+ }
+
+ public static boolean processKeyEvent(KeyEvent event) {
+ if (!isSourceValid(event.getSource()))
+ return false;
+
+ if (isGamepadSource(event.getSource())) {
+ // Dpad return motion event + key event, this remove the key event
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_UP_LEFT:
+ case KeyEvent.KEYCODE_DPAD_UP_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_DOWN_LEFT:
+ case KeyEvent.KEYCODE_DPAD_DOWN_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ return true;
+ }
+ }
+ handleEvent(new InputEvent(KeyEvent.keyCodeToString(event.getKeyCode()), event.getAction() == KeyEvent.ACTION_UP ? 0.0f : 1.0f));
+ return true;
+ }
+
+ public static void reset() {
+ eventListener = null;
+ motionDeadZone = 0.0f;
+ motionDownEvents.clear();
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputMap.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputMap.java
new file mode 100644
index 00000000..6e61345c
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputMap.java
@@ -0,0 +1,42 @@
+package com.panda3ds.pandroid.input;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.panda3ds.pandroid.app.PandroidApplication;
+import com.panda3ds.pandroid.utils.Constants;
+
+public class InputMap {
+
+ private static SharedPreferences data;
+ private static final String KEY_DEAD_ZONE = "deadZone";
+
+ public static void initialize() {
+ data = PandroidApplication.getAppContext().getSharedPreferences(Constants.PREF_INPUT_MAP, Context.MODE_PRIVATE);
+ }
+
+ public static float getDeadZone() {
+ return data.getFloat(KEY_DEAD_ZONE, 0.2f);
+ }
+
+ public static void set(KeyName key, String name) {
+ data.edit().putString(key.name(), name).apply();
+ }
+
+ public static String relative(KeyName key) {
+ return data.getString(key.name(), "-");
+ }
+
+ public static KeyName relative(String name) {
+ for (KeyName key : KeyName.values()) {
+ if (relative(key).equalsIgnoreCase(name))
+ return key;
+ }
+ return KeyName.NULL;
+ }
+
+ public static void setDeadZone(float value) {
+ data.edit().putFloat(KEY_DEAD_ZONE, Math.max(0, Math.min(1.0F, value))).apply();
+ }
+
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java
new file mode 100644
index 00000000..1253529f
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java
@@ -0,0 +1,38 @@
+package com.panda3ds.pandroid.input;
+
+import com.panda3ds.pandroid.utils.Constants;
+
+public enum KeyName {
+ A(Constants.INPUT_KEY_A),
+ B(Constants.INPUT_KEY_B),
+ X(Constants.INPUT_KEY_X),
+ Y(Constants.INPUT_KEY_Y),
+ UP(Constants.INPUT_KEY_UP),
+ DOWN(Constants.INPUT_KEY_DOWN),
+ LEFT(Constants.INPUT_KEY_LEFT),
+ RIGHT(Constants.INPUT_KEY_RIGHT),
+ AXIS_LEFT,
+ AXIS_RIGHT,
+ AXIS_UP,
+ AXIS_DOWN,
+ START(Constants.INPUT_KEY_START),
+ SELECT(Constants.INPUT_KEY_SELECT),
+ L(Constants.INPUT_KEY_L),
+ R(Constants.INPUT_KEY_R),
+ NULL;
+
+ private final int keyId;
+
+ KeyName() {
+ this(-1);
+ }
+
+ KeyName(int keyId) {
+ this.keyId = keyId;
+ }
+
+ public int getKeyId() {
+ return keyId;
+ }
+
+}
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Function.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Function.java
new file mode 100644
index 00000000..25a15875
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Function.java
@@ -0,0 +1,5 @@
+package com.panda3ds.pandroid.lang;
+
+public interface Function {
+ void run(T arg);
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java
index 1aac0a4d..7adf2e47 100644
--- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java
@@ -19,7 +19,10 @@ public class Constants {
public static final int N3DS_HALF_HEIGHT = N3DS_FULL_HEIGHT / 2;
public static final String ACTIVITY_PARAMETER_PATH = "path";
+ public static final String ACTIVITY_PARAMETER_FRAGMENT = "fragment";
public static final String LOG_TAG = "pandroid";
public static final String PREF_GLOBAL_CONFIG = "app.GlobalConfig";
+ public static final String PREF_GAME_UTILS = "app.GameUtils";
+ public static final String PREF_INPUT_MAP = "app.InputMap";
}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java
new file mode 100644
index 00000000..4ebd7241
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java
@@ -0,0 +1,63 @@
+package com.panda3ds.pandroid.utils;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.documentfile.provider.DocumentFile;
+
+import com.panda3ds.pandroid.app.PandroidApplication;
+
+public class FileUtils {
+
+ public static final String MODE_READ = "r";
+
+ private static Uri parseUri(String value) {
+ return Uri.parse(value);
+ }
+
+ private static Context getContext() {
+ return PandroidApplication.getAppContext();
+ }
+
+ public static String getName(String path) {
+ DocumentFile file = DocumentFile.fromSingleUri(getContext(), parseUri(path));
+ return file.getName();
+ }
+
+ public static long getSize(String path) {
+ return DocumentFile.fromSingleUri(getContext(), parseUri(path)).length();
+ }
+
+
+ public static String getCacheDir() {
+ return getContext().getCacheDir().getAbsolutePath();
+ }
+
+ public static void makeUriPermanent(String uri, String mode) {
+
+ int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+ if (mode.toLowerCase().contains("w"))
+ flags &= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+
+ getContext().getContentResolver().takePersistableUriPermission(parseUri(uri), flags);
+ }
+
+ public static int openContentUri(String path, String mode) {
+ try {
+ Uri uri = parseUri(path);
+ ParcelFileDescriptor descriptor = getContext().getContentResolver().openFileDescriptor(uri, mode);
+ int fd = descriptor.getFd();
+ descriptor.detachFd();
+ descriptor.close();
+ return fd;
+ } catch (Exception e) {
+ Log.e(Constants.LOG_TAG, "Error on openContentUri: " + e);
+ }
+
+ return -1;
+ }
+
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java
new file mode 100644
index 00000000..e4c2f53e
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java
@@ -0,0 +1,64 @@
+package com.panda3ds.pandroid.utils;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+
+import com.google.gson.Gson;
+import com.panda3ds.pandroid.app.GameActivity;
+import com.panda3ds.pandroid.app.PandroidApplication;
+import com.panda3ds.pandroid.data.game.GameMetadata;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
+
+public class GameUtils {
+ private static final String KEY_GAME_LIST = "gameList";
+ private static final ArrayList games = new ArrayList<>();
+ private static SharedPreferences data;
+ private static final Gson gson = new Gson();
+
+ public static void initialize() {
+ data = PandroidApplication.getAppContext().getSharedPreferences(Constants.PREF_GAME_UTILS, Context.MODE_PRIVATE);
+
+ GameMetadata[] list = gson.fromJson(data.getString(KEY_GAME_LIST, "[]"), GameMetadata[].class);
+ games.clear();
+ games.addAll(Arrays.asList(list));
+ }
+
+ public static GameMetadata findByRomPath(String romPath) {
+ for (GameMetadata game : games) {
+ if (Objects.equals(romPath, game.getRomPath())) {
+ return game;
+ }
+ }
+ return null;
+ }
+
+ public static void launch(Context context, GameMetadata game) {
+ String path = PathUtils.getPath(Uri.parse(game.getRomPath()));
+ context.startActivity(new Intent(context, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path));
+ }
+
+ public static void removeGame(GameMetadata game) {
+ games.remove(game);
+ saveAll();
+ }
+
+ public static void addGame(GameMetadata game) {
+ games.add(game);
+ saveAll();
+ }
+
+ public static ArrayList getGames() {
+ return new ArrayList<>(games);
+ }
+
+ private static synchronized void saveAll() {
+ data.edit()
+ .putString(KEY_GAME_LIST, gson.toJson(games.toArray(new GameMetadata[0])))
+ .apply();
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PathUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PathUtils.java
index 9bfaa0e4..c4682de2 100644
--- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PathUtils.java
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PathUtils.java
@@ -9,8 +9,11 @@ import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
+import com.panda3ds.pandroid.app.PandroidApplication;
+
public class PathUtils {
- public static String getPath(final Context context, final Uri uri) {
+ public static String getPath(final Uri uri) {
+ final Context context = PandroidApplication.getAppContext();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/SearchAgent.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/SearchAgent.java
new file mode 100644
index 00000000..212c1a1d
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/SearchAgent.java
@@ -0,0 +1,91 @@
+package com.panda3ds.pandroid.utils;
+
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+public class SearchAgent {
+
+ /**
+ * Store all possibles results in map
+ * id->words
+ */
+ private final HashMap searchBuffer = new HashMap<>();
+
+ // Add search item to list
+ public void addToBuffer(String id, String... words) {
+ StringBuilder string = new StringBuilder();
+ for (String word : words) {
+ string.append(normalize(word)).append(" ");
+ }
+ searchBuffer.put(id, string.toString());
+ }
+
+ /**
+ * Convert string to simple string with only a-z 0-9 for do this first it get the input string
+ * and apply lower case, after convert all chars to ASCII
+ * Ex: ç => c, á => a
+ * after replace all double space for single space
+ */
+ private String normalize(String string) {
+ string = Normalizer.normalize(string, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
+
+ return string.toLowerCase()
+ .replaceAll("(?!([a-z0-9 ])).*", "")
+ .replaceAll("\\s\\s", " ");
+ }
+
+ // Execute search and return array with item id.
+ public List search(String query) {
+ String[] words = normalize(query).split("\\s");
+
+ if (words.length == 0)
+ return Collections.emptyList();
+
+ // Map for add all search result: id -> probability
+ HashMap results = new HashMap<>();
+ for (String key : searchBuffer.keySet()) {
+ int probability = 0;
+ String value = searchBuffer.get(key);
+ for (String word : words) {
+ if (value.contains(word))
+ probability++;
+ }
+ if (probability > 0)
+ results.put(key, probability);
+ }
+
+
+ // Filter by probability average
+ // Ex: A = 10% B = 30% C = 70% (calc is (10+30+70)/3=36)
+ // After remove all result with probability < 36
+ int average = 0;
+ for (String key : results.keySet()) {
+ average += results.get(key);
+ }
+ average = average / Math.max(1, results.size());
+
+ int i = 0;
+ ArrayList resultKeys = new ArrayList<>(Arrays.asList(results.keySet().toArray(new String[0])));
+ while ((i < resultKeys.size() && resultKeys.size() > 1)) {
+ if (results.get(resultKeys.get(i)) < average) {
+ String key = resultKeys.get(i);
+ resultKeys.remove(i);
+ results.remove(key);
+ i = 0;
+ continue;
+ }
+ i++;
+ }
+
+ return Arrays.asList(results.keySet().toArray(new String[0]));
+ }
+
+ // Clear search buffer
+ public void clearBuffer() {
+ searchBuffer.clear();
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/SimpleTextWatcher.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/SimpleTextWatcher.java
new file mode 100644
index 00000000..46a43ddb
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/SimpleTextWatcher.java
@@ -0,0 +1,19 @@
+package com.panda3ds.pandroid.view;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+
+public interface SimpleTextWatcher extends TextWatcher {
+ void onChange(String value);
+
+ @Override
+ default void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ default void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ default void afterTextChanged(Editable s){
+ onChange(s.toString());
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java
new file mode 100644
index 00000000..1a3febd4
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java
@@ -0,0 +1,42 @@
+package com.panda3ds.pandroid.view.gamesgrid;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.data.game.GameMetadata;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class GameAdapter extends RecyclerView.Adapter {
+ private final ArrayList games = new ArrayList<>();
+
+ @NonNull
+ @Override
+ public ItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.holder_game, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ItemHolder holder, int position) {
+ holder.apply(games.get(position));
+ }
+
+ public void replace(List games) {
+ int oldCount = getItemCount();
+ this.games.clear();
+ notifyItemRangeRemoved(0, oldCount);
+ this.games.addAll(games);
+ notifyItemRangeInserted(0, getItemCount());
+ }
+
+ @Override
+ public int getItemCount() {
+ return games.size();
+ }
+
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameIconView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameIconView.java
new file mode 100644
index 00000000..df4d2a5c
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameIconView.java
@@ -0,0 +1,42 @@
+package com.panda3ds.pandroid.view.gamesgrid;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageView;
+
+public class GameIconView extends AppCompatImageView {
+
+ public GameIconView(@NonNull Context context) {
+ super(context);
+ }
+
+ public GameIconView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public GameIconView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int size = getMeasuredWidth();
+ setMeasuredDimension(size, size);
+ }
+
+ @Override
+ public void setImageBitmap(Bitmap bm) {
+ super.setImageBitmap(bm);
+ Drawable bitmapDrawable = getDrawable();
+ if (bitmapDrawable instanceof BitmapDrawable) {
+ bitmapDrawable.setFilterBitmap(false);
+ }
+ }
+}
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java
new file mode 100644
index 00000000..d218d467
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java
@@ -0,0 +1,58 @@
+package com.panda3ds.pandroid.view.gamesgrid;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.panda3ds.pandroid.data.game.GameMetadata;
+
+import java.util.List;
+
+public class GamesGridView extends RecyclerView {
+ private int iconSize = 170;
+ private final GameAdapter adapter;
+
+ public GamesGridView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setLayoutManager(new AutoFitLayout());
+ setAdapter(adapter = new GameAdapter());
+ }
+
+ public void setGameList(List games) {
+ adapter.replace(games);
+ }
+
+ public void setIconSize(int iconSize) {
+ this.iconSize = iconSize;
+ requestLayout();
+ measure(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY);
+ }
+
+ private final class AutoFitLayout extends GridLayoutManager {
+ public AutoFitLayout() {
+ super(GamesGridView.this.getContext(), 1);
+ }
+
+ @Override
+ public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec, int heightSpec) {
+ super.onMeasure(recycler, state, widthSpec, heightSpec);
+ int width = getMeasuredWidth();
+ int iconSize = (int) (GamesGridView.this.iconSize * getResources().getDisplayMetrics().density);
+ int iconCount = Math.max(1, width / iconSize);
+ if (getSpanCount() != iconCount)
+ setSpanCount(iconCount);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java
new file mode 100644
index 00000000..d2ccba1f
--- /dev/null
+++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java
@@ -0,0 +1,28 @@
+package com.panda3ds.pandroid.view.gamesgrid;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.panda3ds.pandroid.R;
+import com.panda3ds.pandroid.data.game.GameMetadata;
+import com.panda3ds.pandroid.utils.GameUtils;
+
+class ItemHolder extends RecyclerView.ViewHolder {
+ public ItemHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ public void apply(GameMetadata game) {
+ ((AppCompatTextView) itemView.findViewById(R.id.title))
+ .setText(game.getTitle());
+ ((AppCompatTextView) itemView.findViewById(R.id.description))
+ .setText(game.getPublisher());
+
+ itemView.setOnClickListener((v) -> {
+ GameUtils.launch(v.getContext(), game);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/drawable/ic_key_a.xml b/src/pandroid/app/src/main/res/drawable/ic_key_a.xml
new file mode 100644
index 00000000..3081c462
--- /dev/null
+++ b/src/pandroid/app/src/main/res/drawable/ic_key_a.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/pandroid/app/src/main/res/drawable/ic_search.xml b/src/pandroid/app/src/main/res/drawable/ic_search.xml
new file mode 100644
index 00000000..a5687c63
--- /dev/null
+++ b/src/pandroid/app/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/pandroid/app/src/main/res/drawable/ic_settings.xml b/src/pandroid/app/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 00000000..298a5a1f
--- /dev/null
+++ b/src/pandroid/app/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/pandroid/app/src/main/res/drawable/ic_videogame.xml b/src/pandroid/app/src/main/res/drawable/ic_videogame.xml
new file mode 100644
index 00000000..8693be5f
--- /dev/null
+++ b/src/pandroid/app/src/main/res/drawable/ic_videogame.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/pandroid/app/src/main/res/drawable/search_bar_background.xml b/src/pandroid/app/src/main/res/drawable/search_bar_background.xml
new file mode 100644
index 00000000..44a1c5b4
--- /dev/null
+++ b/src/pandroid/app/src/main/res/drawable/search_bar_background.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_background.xml b/src/pandroid/app/src/main/res/drawable/simple_card_background.xml
new file mode 100644
index 00000000..88845ce4
--- /dev/null
+++ b/src/pandroid/app/src/main/res/drawable/simple_card_background.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_button.xml b/src/pandroid/app/src/main/res/drawable/simple_card_button.xml
deleted file mode 100644
index d58e9c4f..00000000
--- a/src/pandroid/app/src/main/res/drawable/simple_card_button.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- -
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_button_left.xml b/src/pandroid/app/src/main/res/drawable/simple_card_button_left.xml
deleted file mode 100644
index baf1f293..00000000
--- a/src/pandroid/app/src/main/res/drawable/simple_card_button_left.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- -
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_button_right.xml b/src/pandroid/app/src/main/res/drawable/simple_card_button_right.xml
deleted file mode 100644
index 2f69341c..00000000
--- a/src/pandroid/app/src/main/res/drawable/simple_card_button_right.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- -
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/drawable/temp_thumb.jpg b/src/pandroid/app/src/main/res/drawable/temp_thumb.jpg
new file mode 100644
index 00000000..e66782be
Binary files /dev/null and b/src/pandroid/app/src/main/res/drawable/temp_thumb.jpg differ
diff --git a/src/pandroid/app/src/main/res/layout-land/activity_main.xml b/src/pandroid/app/src/main/res/layout-land/activity_main.xml
new file mode 100644
index 00000000..ba552154
--- /dev/null
+++ b/src/pandroid/app/src/main/res/layout-land/activity_main.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/layout/activity_input_map.xml b/src/pandroid/app/src/main/res/layout/activity_input_map.xml
new file mode 100644
index 00000000..cbacc64e
--- /dev/null
+++ b/src/pandroid/app/src/main/res/layout/activity_input_map.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/layout/activity_main.xml b/src/pandroid/app/src/main/res/layout/activity_main.xml
index 89a17ce9..1facabd3 100644
--- a/src/pandroid/app/src/main/res/layout/activity_main.xml
+++ b/src/pandroid/app/src/main/res/layout/activity_main.xml
@@ -1,5 +1,5 @@
-
-
-
-
+
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/layout/activity_preference.xml b/src/pandroid/app/src/main/res/layout/activity_preference.xml
new file mode 100644
index 00000000..54b3d364
--- /dev/null
+++ b/src/pandroid/app/src/main/res/layout/activity_preference.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/layout/fragment_games.xml b/src/pandroid/app/src/main/res/layout/fragment_games.xml
new file mode 100644
index 00000000..ee69fdc8
--- /dev/null
+++ b/src/pandroid/app/src/main/res/layout/fragment_games.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/layout/fragment_search.xml b/src/pandroid/app/src/main/res/layout/fragment_search.xml
new file mode 100644
index 00000000..5872a404
--- /dev/null
+++ b/src/pandroid/app/src/main/res/layout/fragment_search.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/layout/holder_game.xml b/src/pandroid/app/src/main/res/layout/holder_game.xml
new file mode 100644
index 00000000..169896e6
--- /dev/null
+++ b/src/pandroid/app/src/main/res/layout/holder_game.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/layout/preference_simple_about.xml b/src/pandroid/app/src/main/res/layout/preference_simple_about.xml
new file mode 100644
index 00000000..9364de36
--- /dev/null
+++ b/src/pandroid/app/src/main/res/layout/preference_simple_about.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/layout/preference_start_item.xml b/src/pandroid/app/src/main/res/layout/preference_start_item.xml
new file mode 100644
index 00000000..35aa5554
--- /dev/null
+++ b/src/pandroid/app/src/main/res/layout/preference_start_item.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/menu/main_activity_navigation.xml b/src/pandroid/app/src/main/res/menu/main_activity_navigation.xml
new file mode 100644
index 00000000..f457d8cf
--- /dev/null
+++ b/src/pandroid/app/src/main/res/menu/main_activity_navigation.xml
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/values-night/themes.xml b/src/pandroid/app/src/main/res/values-night/themes.xml
deleted file mode 100644
index 98dd320a..00000000
--- a/src/pandroid/app/src/main/res/values-night/themes.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 00000000..90e533a7
--- /dev/null
+++ b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,16 @@
+
+
+ pandroid
+ Carregar ROM
+ Jogos
+ Configurações
+ Pesquisar
+ Desconhecido
+ Esquerda
+ Direita
+ Cima
+ Baixo
+ Outros
+ Pressione qualquer tecla
+ Eixos
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/values/strings.xml b/src/pandroid/app/src/main/res/values/strings.xml
index f937ad5f..b7855eca 100644
--- a/src/pandroid/app/src/main/res/values/strings.xml
+++ b/src/pandroid/app/src/main/res/values/strings.xml
@@ -1,4 +1,17 @@
pandroid
Load ROM
+ Games
+ Settings
+ Search
+ Unknown
+
+ Left
+ Right
+ Up
+ Down
+
+ Others
+ Press any key
+ Axis
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/values/styleable.xml b/src/pandroid/app/src/main/res/values/styleable.xml
index 69219724..32e51685 100644
--- a/src/pandroid/app/src/main/res/values/styleable.xml
+++ b/src/pandroid/app/src/main/res/values/styleable.xml
@@ -9,4 +9,7 @@
- multiply
- #FFF
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/xml/input_map_preferences.xml b/src/pandroid/app/src/main/res/xml/input_map_preferences.xml
new file mode 100644
index 00000000..25b224d1
--- /dev/null
+++ b/src/pandroid/app/src/main/res/xml/input_map_preferences.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pandroid/app/src/main/res/xml/start_preferences.xml b/src/pandroid/app/src/main/res/xml/start_preferences.xml
new file mode 100644
index 00000000..48c8d7a9
--- /dev/null
+++ b/src/pandroid/app/src/main/res/xml/start_preferences.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
\ No newline at end of file