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 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 @@ - - -