Android: Implementation of basic game list
This commit is contained in:
@ -0,0 +1,8 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
public enum ConsoleRegion {
|
||||
AutoDetect,
|
||||
NTSC_J,
|
||||
NTSC_U,
|
||||
PAL
|
||||
}
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user