From 102361174a1abcaa4ab5535a0bc7478c560586c1 Mon Sep 17 00:00:00 2001 From: GabrielBRDeveloper Date: Fri, 15 Dec 2023 11:11:53 -0400 Subject: [PATCH 01/21] paris changes --- .../com/panda3ds/pandroid/data/config/GlobalConfig.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java index d1427dba..185eb9d1 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java @@ -37,13 +37,13 @@ public class GlobalConfig { if (value instanceof String) { data.edit().putString(key.name, (String) value).apply(); } else if (value instanceof Integer) { - data.edit().putInt(key.name, (Integer) value).apply(); + data.edit().putInt(key.name, (int) value).apply(); } else if (value instanceof Boolean) { - data.edit().putBoolean(key.name, (Boolean) value).apply(); + data.edit().putBoolean(key.name, (boolean) value).apply(); } else if (value instanceof Long) { - data.edit().putLong(key.name, (Long) value).apply(); + data.edit().putLong(key.name, (long) value).apply(); } else if (value instanceof Float) { - data.edit().putFloat(key.name, (Float) value).apply(); + data.edit().putFloat(key.name, (float) value).apply(); } else { throw new IllegalArgumentException("Invalid global config value instance"); } From ab37cb571d7574db0dba94ede7af1ac4730a1763 Mon Sep 17 00:00:00 2001 From: GabrielBRDeveloper Date: Fri, 15 Dec 2023 11:12:48 -0400 Subject: [PATCH 02/21] paris changes --- .../java/com/panda3ds/pandroid/data/config/GlobalConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java index 185eb9d1..ddfee09e 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java @@ -32,7 +32,6 @@ public class GlobalConfig { return (T) value; } - //Need synchronized why SharedPreferences don't support aysnc write public static synchronized void set(Key key, T value) { if (value instanceof String) { data.edit().putString(key.name, (String) value).apply(); From 3298bd14dbc78055afef2157c65ce4bd8ee5b7bc Mon Sep 17 00:00:00 2001 From: GabrielBRDeveloper Date: Sat, 16 Dec 2023 23:22:11 -0400 Subject: [PATCH 03/21] Pandroid UI --- src/pandroid/app/build.gradle.kts | 2 + src/pandroid/app/src/main/AndroidManifest.xml | 9 +- .../panda3ds/pandroid/app/GameActivity.java | 35 ++++- .../panda3ds/pandroid/app/MainActivity.java | 58 +++++--- .../pandroid/app/PandroidApplication.java | 4 + .../pandroid/app/PreferenceActivity.java | 54 ++++++++ .../app/base/BasePreferenceFragment.java | 19 +++ .../pandroid/app/game/AlberInputListener.java | 54 ++++++++ .../pandroid/app/main/GamesFragment.java | 75 +++++++++++ .../pandroid/app/main/SearchFragment.java | 67 ++++++++++ .../pandroid/app/main/SettingsFragment.java | 18 +++ .../app/preferences/InputMapActivity.java | 76 +++++++++++ .../app/preferences/InputMapPreferences.java | 72 ++++++++++ .../pandroid/data/game/GameMetadata.java | 44 ++++++ .../pandroid/data/game/GameRegion.java | 12 ++ .../panda3ds/pandroid/input/InputEvent.java | 23 ++++ .../panda3ds/pandroid/input/InputHandler.java | 116 ++++++++++++++++ .../com/panda3ds/pandroid/input/InputMap.java | 42 ++++++ .../com/panda3ds/pandroid/input/KeyName.java | 38 ++++++ .../com/panda3ds/pandroid/lang/Function.java | 5 + .../panda3ds/pandroid/utils/Constants.java | 3 + .../panda3ds/pandroid/utils/FileUtils.java | 63 +++++++++ .../panda3ds/pandroid/utils/GameUtils.java | 64 +++++++++ .../panda3ds/pandroid/utils/PathUtils.java | 5 +- .../panda3ds/pandroid/utils/SearchAgent.java | 91 +++++++++++++ .../pandroid/view/SimpleTextWatcher.java | 19 +++ .../pandroid/view/gamesgrid/GameAdapter.java | 42 ++++++ .../pandroid/view/gamesgrid/GameIconView.java | 42 ++++++ .../view/gamesgrid/GamesGridView.java | 58 ++++++++ .../pandroid/view/gamesgrid/ItemHolder.java | 28 ++++ .../app/src/main/res/drawable/ic_key_a.xml | 5 + .../app/src/main/res/drawable/ic_search.xml | 5 + .../app/src/main/res/drawable/ic_settings.xml | 5 + .../src/main/res/drawable/ic_videogame.xml | 5 + .../res/drawable/search_bar_background.xml | 9 ++ .../res/drawable/simple_card_background.xml | 9 ++ .../main/res/drawable/simple_card_button.xml | 12 -- .../res/drawable/simple_card_button_left.xml | 12 -- .../res/drawable/simple_card_button_right.xml | 12 -- .../app/src/main/res/drawable/temp_thumb.jpg | Bin 0 -> 51816 bytes .../main/res/layout-land/activity_main.xml | 29 ++++ .../main/res/layout/activity_input_map.xml | 23 ++++ .../app/src/main/res/layout/activity_main.xml | 32 +++-- .../main/res/layout/activity_preference.xml | 19 +++ .../src/main/res/layout/fragment_games.xml | 25 ++++ .../src/main/res/layout/fragment_search.xml | 49 +++++++ .../app/src/main/res/layout/holder_game.xml | 41 ++++++ .../res/layout/preference_simple_about.xml | 29 ++++ .../main/res/layout/preference_start_item.xml | 54 ++++++++ .../res/menu/main_activity_navigation.xml | 24 ++++ .../app/src/main/res/values-night/themes.xml | 7 - .../src/main/res/values-pt-rBR/strings.xml | 16 +++ .../app/src/main/res/values/strings.xml | 13 ++ .../app/src/main/res/values/styleable.xml | 3 + .../main/res/xml/input_map_preferences.xml | 125 ++++++++++++++++++ .../src/main/res/xml/start_preferences.xml | 17 +++ 56 files changed, 1741 insertions(+), 77 deletions(-) create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapActivity.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputEvent.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputMap.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Function.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/SearchAgent.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/SimpleTextWatcher.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameIconView.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java create mode 100644 src/pandroid/app/src/main/res/drawable/ic_key_a.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_search.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_settings.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_videogame.xml create mode 100644 src/pandroid/app/src/main/res/drawable/search_bar_background.xml create mode 100644 src/pandroid/app/src/main/res/drawable/simple_card_background.xml delete mode 100644 src/pandroid/app/src/main/res/drawable/simple_card_button.xml delete mode 100644 src/pandroid/app/src/main/res/drawable/simple_card_button_left.xml delete mode 100644 src/pandroid/app/src/main/res/drawable/simple_card_button_right.xml create mode 100644 src/pandroid/app/src/main/res/drawable/temp_thumb.jpg create mode 100644 src/pandroid/app/src/main/res/layout-land/activity_main.xml create mode 100644 src/pandroid/app/src/main/res/layout/activity_input_map.xml create mode 100644 src/pandroid/app/src/main/res/layout/activity_preference.xml create mode 100644 src/pandroid/app/src/main/res/layout/fragment_games.xml create mode 100644 src/pandroid/app/src/main/res/layout/fragment_search.xml create mode 100644 src/pandroid/app/src/main/res/layout/holder_game.xml create mode 100644 src/pandroid/app/src/main/res/layout/preference_simple_about.xml create mode 100644 src/pandroid/app/src/main/res/layout/preference_start_item.xml create mode 100644 src/pandroid/app/src/main/res/menu/main_activity_navigation.xml delete mode 100644 src/pandroid/app/src/main/res/values-night/themes.xml create mode 100644 src/pandroid/app/src/main/res/values-pt-rBR/strings.xml create mode 100644 src/pandroid/app/src/main/res/xml/input_map_preferences.xml create mode 100644 src/pandroid/app/src/main/res/xml/start_preferences.xml 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 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 0000000000000000000000000000000000000000..e66782be3443db4a00c8ea22027718fb270ae2bf GIT binary patch literal 51816 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 Zo*cb)>0A0uT}#zdIh7(ATk8Mc1OO)CwEX}8 literal 0 HcmV?d00001 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 @@ - - -