Android: Multiple improvements

- Save/resume state when closing/starting.
 - Error reporting - hopefully can figure out why it's not starting on
   some devices.
 - Reduce startup latency.
 - Add more options and descriptions to settings.
This commit is contained in:
Connor McLaughlin
2020-08-30 15:34:08 +10:00
parent 1eac603c79
commit cbbf599e4e
55 changed files with 1426 additions and 995 deletions

View File

@ -4,37 +4,57 @@ import android.content.Context;
import android.os.Environment;
import android.util.Log;
import android.view.Surface;
import android.widget.Toast;
public class AndroidHostInterface
{
private long nativePointer;
import com.google.android.material.snackbar.Snackbar;
public class AndroidHostInterface {
private long mNativePointer;
private Context mContext;
static public native AndroidHostInterface create(Context context, String userDirectory);
public AndroidHostInterface(long nativePointer)
{
this.nativePointer = nativePointer;
public AndroidHostInterface(Context context) {
this.mContext = context;
}
public void reportError(String message) {
Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
}
public void reportMessage(String message) {
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
}
public native boolean isEmulationThreadRunning();
public native boolean startEmulationThread(Surface surface, String filename, String state_filename);
public native boolean startEmulationThread(EmulationActivity emulationActivity, Surface surface, String filename, boolean resumeState, String state_filename);
public native void stopEmulationThread();
public native void surfaceChanged(Surface surface, int format, int width, int height);
// TODO: Find a better place for this.
public native void setControllerType(int index, String typeName);
public native void setControllerButtonState(int index, int buttonCode, boolean pressed);
public native void setControllerAxisState(int index, int axisCode, float value);
public static native int getControllerButtonCode(String controllerType, String buttonName);
public static native int getControllerAxisCode(String controllerType, String axisName);
public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase);
public native GameListEntry[] getGameListEntries();
public native void resetSystem();
public native void loadState(boolean global, int slot);
public native void saveState(boolean global, int slot);
public native void applySettings();
static {
@ -42,6 +62,7 @@ public class AndroidHostInterface
}
static private AndroidHostInterface mInstance;
static public boolean createInstance(Context context) {
// Set user path.
String externalStorageDirectory = Environment.getExternalStorageDirectory().getAbsolutePath();
@ -57,6 +78,7 @@ public class AndroidHostInterface
static public boolean hasInstance() {
return mInstance != null;
}
static public AndroidHostInterface getInstance() {
return mInstance;
}

View File

@ -3,6 +3,7 @@ package com.github.stenzek.duckstation;
import android.annotation.SuppressLint;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
@ -11,14 +12,12 @@ import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.MenuItem;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager;
/**
@ -30,77 +29,69 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
* Settings interfaces.
*/
SharedPreferences mPreferences;
private boolean getBooleanSetting(String key, boolean defaultValue) {
return mPreferences.getBoolean(key, defaultValue);
}
private void setBooleanSetting(String key, boolean value) {
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean(key, value);
editor.apply();
}
private String getStringSetting(String key, String defaultValue) {
return mPreferences.getString(key, defaultValue);
}
/**
* Touchscreen controller overlay
*/
TouchscreenControllerView mTouchscreenController;
private boolean mTouchscreenControllerVisible = true;
public void reportError(String message) {
Log.e("EmulationActivity", message);
/**
* Whether or not the system UI should be auto-hidden after
* {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
*/
private static final boolean AUTO_HIDE = true;
Object lock = new Object();
runOnUiThread(() -> {
// Toast.makeText(this, message, Toast.LENGTH_LONG);
new AlertDialog.Builder(this)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("OK", (dialog, button) -> {
dialog.dismiss();
synchronized (lock) {
lock.notify();
}
})
.create()
.show();
});
/**
* If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after
* user interaction before hiding the system UI.
*/
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
/**
* Some older devices needs a small delay between UI widget updates
* and a change of the status and navigation bar.
*/
private static final int UI_ANIMATION_DELAY = 300;
private final Handler mHideHandler = new Handler();
private EmulationSurfaceView mContentView;
private final Runnable mHidePart2Runnable = new Runnable() {
@SuppressLint("InlinedApi")
@Override
public void run() {
// Delayed removal of status and navigation bar
// Note that some of these constants are new as of API 16 (Jelly Bean)
// and API 19 (KitKat). It is safe to use them, as they are inlined
// at compile-time and do nothing on earlier devices.
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}
};
private final Runnable mShowPart2Runnable = new Runnable() {
@Override
public void run() {
// Delayed display of UI elements
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.show();
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
}
}
};
private boolean mVisible;
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
hide();
}
};
}
public void reportMessage(String message) {
Log.i("EmulationActivity", message);
runOnUiThread(() -> {
Toast.makeText(this, message, Toast.LENGTH_SHORT);
});
}
public void onEmulationStarted() {
}
public void onEmulationStopped() {
runOnUiThread(() -> {
finish();
});
}
public void onGameTitleChanged(String title) {
runOnUiThread(() -> {
setTitle(title);
});
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
@ -114,16 +105,11 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
return;
}
String bootPath = getIntent().getStringExtra("bootPath");
String bootSaveStatePath = getIntent().getStringExtra("bootSaveStatePath");
boolean resumeState = getIntent().getBooleanExtra("resumeState", false);
final String bootPath = getIntent().getStringExtra("bootPath");
final boolean resumeState = getIntent().getBooleanExtra("resumeState", false);
final String bootSaveStatePath = getIntent().getStringExtra("saveStatePath");
if (!AndroidHostInterface.getInstance()
.startEmulationThread(holder.getSurface(), bootPath, bootSaveStatePath)) {
Log.e("EmulationActivity", "Failed to start emulation thread");
finishActivity(0);
return;
}
AndroidHostInterface.getInstance().startEmulationThread(this, holder.getSurface(), bootPath, resumeState, bootSaveStatePath);
}
@Override
@ -146,14 +132,14 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
actionBar.setDisplayHomeAsUpEnabled(true);
}
mVisible = true;
mSystemUIVisible = true;
mContentView = findViewById(R.id.fullscreen_content);
mContentView.getHolder().addCallback(this);
mContentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mVisible)
hide();
if (mSystemUIVisible)
hideSystemUI();
}
});
@ -173,11 +159,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
hideSystemUI();
}
// Trigger the initial hide() shortly after the activity has been
// created, to briefly hint to the user that UI controls
// are available.
delayedHide(100);
@Override
protected void onStop() {
super.onStop();
if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
AndroidHostInterface.getInstance().stopEmulationThread();
}
}
@Override
@ -228,37 +219,79 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
@Override
public void onBackPressed() {
if (mVisible) {
if (mSystemUIVisible) {
finish();
return;
}
show();
showSystemUI();
}
private void hide() {
/**
* Some older devices needs a small delay between UI widget updates
* and a change of the status and navigation bar.
*/
private static final int UI_ANIMATION_DELAY = 300;
private final Handler mSystemUIHideHandler = new Handler();
private EmulationSurfaceView mContentView;
private final Runnable mHidePart2Runnable = new Runnable() {
@SuppressLint("InlinedApi")
@Override
public void run() {
// Delayed removal of status and navigation bar
// Note that some of these constants are new as of API 16 (Jelly Bean)
// and API 19 (KitKat). It is safe to use them, as they are inlined
// at compile-time and do nothing on earlier devices.
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}
};
private final Runnable mShowPart2Runnable = new Runnable() {
@Override
public void run() {
// Delayed display of UI elements
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.show();
}
}
};
private boolean mSystemUIVisible;
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
hideSystemUI();
}
};
private void hideSystemUI() {
// Hide UI first
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
mVisible = false;
mSystemUIVisible = false;
// Schedule a runnable to remove the status and navigation bar after a delay
mHideHandler.removeCallbacks(mShowPart2Runnable);
mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY);
mSystemUIHideHandler.removeCallbacks(mShowPart2Runnable);
mSystemUIHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY);
}
@SuppressLint("InlinedApi")
private void show() {
private void showSystemUI() {
// Show the system bar
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
mVisible = true;
mSystemUIVisible = true;
// Schedule a runnable to display UI elements after a delay
mHideHandler.removeCallbacks(mHidePart2Runnable);
mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);
mSystemUIHideHandler.removeCallbacks(mHidePart2Runnable);
mSystemUIHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);
}
/**
@ -266,10 +299,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
* previously scheduled calls.
*/
private void delayedHide(int delayMillis) {
mHideHandler.removeCallbacks(mHideRunnable);
mHideHandler.postDelayed(mHideRunnable, delayMillis);
mSystemUIHideHandler.removeCallbacks(mHideRunnable);
mSystemUIHideHandler.postDelayed(mHideRunnable, delayMillis);
}
/**
* Touchscreen controller overlay
*/
TouchscreenControllerView mTouchscreenController;
private boolean mTouchscreenControllerVisible = true;
private void setTouchscreenControllerVisibility(boolean visible) {
mTouchscreenControllerVisible = visible;
mTouchscreenController.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);

View File

@ -17,13 +17,13 @@ import java.lang.reflect.Array;
import java.lang.reflect.Method;
public final class FileUtil {
static String TAG="TAG";
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);
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con);
if (volumePath == null) return File.separator;
if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1);
@ -37,8 +37,7 @@ public final class FileUtil {
return volumePath + documentPath;
else
return volumePath + File.separator + documentPath;
}
else return volumePath;
} else return volumePath;
}

View File

@ -7,14 +7,12 @@ import android.widget.TextView;
import androidx.core.content.ContextCompat;
public class GameListEntry {
public enum EntryType
{
public enum EntryType {
Disc,
PSExe
}
public enum CompatibilityRating
{
public enum CompatibilityRating {
Unknown,
DoesntBoot,
CrashesInIntro,
@ -72,15 +70,21 @@ public class GameListEntry {
return mTitle;
}
public String getModifiedTime() { return mModifiedTime; }
public String getModifiedTime() {
return mModifiedTime;
}
public DiscRegion getRegion() {
return mRegion;
}
public EntryType getType() { return mType; }
public EntryType getType() {
return mType;
}
public CompatibilityRating getCompatibilityRating() { return mCompatibilityRating; }
public CompatibilityRating getCompatibilityRating() {
return mCompatibilityRating;
}
public void fillView(View view) {
((TextView) view.findViewById(R.id.game_list_view_entry_title)).setText(mTitle);

View File

@ -28,27 +28,26 @@ import android.view.MenuItem;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.Toast;
import java.util.HashSet;
import java.util.Set;
import java.util.prefs.Preferences;
import static com.google.android.material.snackbar.Snackbar.make;
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;
private boolean mHasExternalStoragePermissions = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) {
Log.i("MainActivity", "Failed to create host interface");
throw new RuntimeException("Failed to create host interface");
}
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@ -93,6 +92,18 @@ public class MainActivity extends AppCompatActivity {
return true;
}
});
mHasExternalStoragePermissions = checkForExternalStoragePermissions();
if (mHasExternalStoragePermissions)
completeStartup();
}
private void completeStartup() {
if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) {
Log.i("MainActivity", "Failed to create host interface");
throw new RuntimeException("Failed to create host interface");
}
mGameList.refresh(false, false);
}
@ -122,12 +133,17 @@ public class MainActivity extends AppCompatActivity {
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_add_game_directory) {
if (id == R.id.action_resume) {
startEmulation(null, true);
} else if (id == R.id.action_start_bios) {
startEmulation(null, false);
} else if (id == R.id.action_add_game_directory) {
startAddGameDirectory();
} else if (id == R.id.action_scan_for_new_games) {
mGameList.refresh(false, false);
} if (id == R.id.action_rescan_all_games) {
mGameList.refresh(true, false);
}
if (id == R.id.action_rescan_all_games) {
mGameList.refresh(true, true);
}
if (id == R.id.action_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
@ -190,19 +206,21 @@ public class MainActivity extends AppCompatActivity {
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);
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
if (!mHasExternalStoragePermissions) {
mHasExternalStoragePermissions = true;
completeStartup();
}
} else {
Toast.makeText(this,
"External storage permissions are required to use DuckStation.",
Toast.LENGTH_LONG);
finish();
}
}
}
private boolean startEmulation(String bootPath, boolean resumeState) {
if (!checkForExternalStoragePermissions()) {
return false;
}
Intent intent = new Intent(this, EmulationActivity.class);
intent.putExtra("bootPath", bootPath);
intent.putExtra("resumeState", resumeState);

View File

@ -19,25 +19,24 @@ public class TouchscreenControllerButtonView extends View {
private String mButtonName = "";
private ButtonStateChangedListener mListener;
public interface ButtonStateChangedListener
{
public interface ButtonStateChangedListener {
void onButtonStateChanged(TouchscreenControllerButtonView view, boolean pressed);
}
public TouchscreenControllerButtonView(Context context) {
super(context);
init(context,null, 0);
init(context, null, 0);
}
public TouchscreenControllerButtonView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context,attrs, 0);
init(context, attrs, 0);
}
public TouchscreenControllerButtonView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context,attrs, defStyle);
init(context, attrs, defStyle);
}
private void init(Context context, AttributeSet attrs, int defStyle) {
@ -80,15 +79,12 @@ public class TouchscreenControllerButtonView extends View {
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
public boolean onTouchEvent(MotionEvent event) {
final boolean oldState = mPressed;
switch (event.getAction())
{
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
{
case MotionEvent.ACTION_POINTER_DOWN: {
mPressed = true;
invalidate();
@ -99,8 +95,7 @@ public class TouchscreenControllerButtonView extends View {
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
{
case MotionEvent.ACTION_POINTER_UP: {
mPressed = false;
invalidate();
@ -114,8 +109,7 @@ public class TouchscreenControllerButtonView extends View {
return super.onTouchEvent(event);
}
public boolean isPressed()
{
public boolean isPressed() {
return mPressed;
}
@ -127,13 +121,11 @@ public class TouchscreenControllerButtonView extends View {
mButtonName = buttonName;
}
public int getButtonCode()
{
public int getButtonCode() {
return mButtonCode;
}
public void setButtonCode(int code)
{
public void setButtonCode(int code) {
mButtonCode = code;
}
@ -153,8 +145,7 @@ public class TouchscreenControllerButtonView extends View {
mUnpressedDrawable = unpressedDrawable;
}
public void setButtonStateChangedListener(ButtonStateChangedListener listener)
{
public void setButtonStateChangedListener(ButtonStateChangedListener listener) {
mListener = listener;
}
}

View File

@ -50,9 +50,8 @@ public class TouchscreenControllerView extends FrameLayout implements Touchscree
linkButton(view, R.id.controller_button_r2, "R2");
}
private void linkButton(View view, int id, String buttonName)
{
TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView)view.findViewById(id);
private void linkButton(View view, int id, String buttonName) {
TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id);
buttonView.setButtonName(buttonName);
buttonView.setButtonStateChangedListener(this);