Android: Implementation of basic game list

This commit is contained in:
Connor McLaughlin
2019-12-02 01:09:25 +10:00
parent adc3a2fac1
commit 6da9e23d3b
16 changed files with 938 additions and 38 deletions

View File

@ -0,0 +1,8 @@
package com.github.stenzek.duckstation;
public enum ConsoleRegion {
AutoDetect,
NTSC_J,
NTSC_U,
PAL
}

View File

@ -21,7 +21,9 @@ import androidx.core.app.NavUtils;
* status bar and navigation/system bar) with user interaction.
*/
public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback {
/** Interface to the native emulator core */
/**
* Interface to the native emulator core
*/
AndroidHostInterface mHostInterface;
/**
@ -104,11 +106,13 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
return;
}
String filename = new String();
String state_filename = new String();
if (!mHostInterface.startEmulationThread(holder.getSurface(),filename, state_filename))
{
String bootPath = getIntent().getStringExtra("bootPath");
String bootSaveStatePath = getIntent().getStringExtra("bootSaveStatePath");
if (!mHostInterface
.startEmulationThread(holder.getSurface(), bootPath, bootSaveStatePath)) {
Log.e("EmulationActivity", "Failed to start emulation thread");
finishActivity(0);
return;
}
}
@ -133,7 +137,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
}
mVisible = true;
mContentView = (SurfaceView)findViewById(R.id.fullscreen_content);
mContentView = (SurfaceView) findViewById(R.id.fullscreen_content);
Log.e("EmulationActivity", "adding callback");
mContentView.getHolder().addCallback(this);

View File

@ -0,0 +1,95 @@
package com.github.stenzek.duckstation;
// https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import androidx.annotation.Nullable;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
public final class FileUtil {
static String TAG="TAG";
private static final String PRIMARY_VOLUME_NAME = "primary";
@Nullable
public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
if (treeUri == null) return null;
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),con);
if (volumePath == null) return File.separator;
if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1);
String documentPath = getDocumentPathFromTreeUri(treeUri);
if (documentPath.endsWith(File.separator))
documentPath = documentPath.substring(0, documentPath.length() - 1);
if (documentPath.length() > 0) {
if (documentPath.startsWith(File.separator))
return volumePath + documentPath;
else
return volumePath + File.separator + documentPath;
}
else return volumePath;
}
@SuppressLint("ObsoleteSdkInt")
private static String getVolumePath(final String volumeId, Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null;
try {
StorageManager mStorageManager =
(StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
Method getUuid = storageVolumeClazz.getMethod("getUuid");
Method getPath = storageVolumeClazz.getMethod("getPath");
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
Object result = getVolumeList.invoke(mStorageManager);
final int length = Array.getLength(result);
for (int i = 0; i < length; i++) {
Object storageVolumeElement = Array.get(result, i);
String uuid = (String) getUuid.invoke(storageVolumeElement);
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
// primary volume?
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement);
// other volumes?
if (uuid != null && uuid.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement);
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if (split.length > 0) return split[0];
else return null;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final String[] split = docId.split(":");
if ((split.length >= 2) && (split[1] != null)) return split[1];
else return File.separator;
}
}

View File

@ -0,0 +1,102 @@
package com.github.stenzek.duckstation;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.ArraySet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import androidx.preference.PreferenceManager;
import java.util.Set;
public class GameList {
static {
System.loadLibrary("duckstation-native");
}
private Context mContext;
private String mCachePath;
private String mRedumpDatPath;
private String[] mSearchDirectories;
private boolean mSearchRecursively;
private GameListEntry[] mEntries;
static private native GameListEntry[] getEntries(String cachePath, String redumpDatPath,
String[] searchDirectories,
boolean searchRecursively);
public GameList(Context context) {
mContext = context;
refresh();
}
public void refresh() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
mCachePath = preferences.getString("GameList/CachePath", "");
mRedumpDatPath = preferences.getString("GameList/RedumpDatPath", "");
Set<String> searchDirectories =
preferences.getStringSet("GameList/SearchDirectories", null);
if (searchDirectories != null) {
mSearchDirectories = new String[searchDirectories.size()];
searchDirectories.toArray(mSearchDirectories);
} else {
mSearchDirectories = new String[0];
}
mSearchRecursively = preferences.getBoolean("GameList/SearchRecursively", true);
// Search and get entries from native code
mEntries = getEntries(mCachePath, mRedumpDatPath, mSearchDirectories, mSearchRecursively);
}
public int getEntryCount() {
return mEntries.length;
}
public GameListEntry getEntry(int index) {
return mEntries[index];
}
private class ListViewAdapter extends BaseAdapter {
@Override
public int getCount() {
return mEntries.length;
}
@Override
public Object getItem(int position) {
return mEntries[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext)
.inflate(R.layout.game_list_view_entry, parent, false);
}
mEntries[position].fillView(convertView);
return convertView;
}
}
public BaseAdapter getListViewAdapter() {
return new ListViewAdapter();
}
}

View File

@ -0,0 +1,69 @@
package com.github.stenzek.duckstation;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
public class GameListEntry {
private String mPath;
private String mCode;
private String mTitle;
private ConsoleRegion mRegion;
private long mSize;
public GameListEntry(String path, String code, String title, String region, long size) {
mPath = path;
mCode = code;
mTitle = title;
mSize = size;
try {
mRegion = ConsoleRegion.valueOf(region);
} catch (IllegalArgumentException e) {
mRegion = ConsoleRegion.NTSC_U;
}
}
public String getPath() {
return mPath;
}
public String getCode() {
return mCode;
}
public String getTitle() {
return mTitle;
}
public ConsoleRegion getRegion() {
return mRegion;
}
public void fillView(View view) {
((TextView) view.findViewById(R.id.game_list_view_entry_title)).setText(mTitle);
((TextView) view.findViewById(R.id.game_list_view_entry_path)).setText(mPath);
String sizeString = String.format("%.2f MB", (double) mSize / 1048576.0);
((TextView) view.findViewById(R.id.game_list_view_entry_size)).setText(sizeString);
int drawableId;
switch (mRegion) {
case NTSC_J:
drawableId = R.drawable.flag_jp;
break;
case NTSC_U:
default:
drawableId = R.drawable.flag_us;
break;
case PAL:
drawableId = R.drawable.flag_eu;
break;
}
((ImageView) view.findViewById(R.id.game_list_view_entry_region_icon))
.setImageDrawable(ContextCompat.getDrawable(view.getContext(), drawableId));
}
}

View File

@ -1,7 +1,9 @@
package com.github.stenzek.duckstation;
import android.Manifest;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@ -11,13 +13,32 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.preference.PreferenceManager;
import android.content.Intent;
import androidx.collection.ArraySet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.PopupMenu;
import java.util.HashSet;
import java.util.Set;
import java.util.prefs.Preferences;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_EXTERNAL_STORAGE_PERMISSIONS = 1;
private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 2;
private GameList mGameList;
private ListView mGameListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -30,9 +51,39 @@ public class MainActivity extends AppCompatActivity {
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/*Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();*/
startEmulation("nonexistant.cue");
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
i.addCategory(Intent.CATEGORY_DEFAULT);
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
startActivityForResult(Intent.createChooser(i, "Choose directory"),
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
}
});
// Set up game list view.
mGameList = new GameList(this);
mGameListView = findViewById(R.id.game_list_view);
mGameListView.setAdapter(mGameList.getListViewAdapter());
mGameListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
startEmulation(mGameList.getEntry(position).getPath());
}
});
mGameListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position,
long id) {
PopupMenu menu = new PopupMenu(MainActivity.this, view,
Gravity.RIGHT | Gravity.TOP);
menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu());
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
return false;
}
});
menu.show();
return true;
}
});
}
@ -61,22 +112,71 @@ public class MainActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
if (resultCode != RESULT_OK)
return;
Uri treeUri = data.getData();
String path = FileUtil.getFullPathFromTreeUri(treeUri, this);
if (path.length() < 5) {
// sanity check for non-external paths.. do we need permissions or something?
return;
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Set<String> currentValues = prefs.getStringSet("GameList/SearchDirectories", null);
if (currentValues == null)
currentValues = new HashSet<String>();
currentValues.add(path);
SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet("GameList/SearchDirectories", currentValues);
editor.apply();
Log.i("MainActivity", "Added path '" + path + "' to game list search directories");
mGameList.refresh();
}
break;
}
}
private boolean checkForExternalStoragePermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
{
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED &&
ContextCompat
.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED) {
return true;
}
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_EXTERNAL_STORAGE_PERMISSIONS);
return false;
}
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
// check that all were successful
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
Snackbar.make(mGameListView,
"External storage permissions are required to start emulation.",
Snackbar.LENGTH_LONG);
}
}
}
private boolean startEmulation(String bootPath) {
if (!checkForExternalStoragePermissions()) {
Snackbar.make(findViewById(R.id.fab), "External storage permissions are required to start emulation.", Snackbar.LENGTH_LONG);
return false;
}
Intent intent = new Intent(this, EmulationActivity.class);
intent.putExtra("bootPath", bootPath);
startActivity(intent);

View File

@ -1,12 +0,0 @@
package com.github.stenzek.duckstation;
public class NativeLibrary {
static
{
System.loadLibrary("duckstation-native");
}
public native boolean createSystem();
public native boolean bootSystem(String filename, String stateFilename);
public native void runFrame();
}