Browse Source

add retro emulator config and prefs

wangyongj 2 năm trước cách đây
mục cha
commit
9890ecd1cd

+ 3 - 0
.gitignore

@@ -1,6 +1,9 @@
 *.iml
 .gradle
 .idea
+/app/src/main/assets
+/app/src/main/libs
+/app/build
 /local.properties
 /.idea/caches
 /.idea/libraries

+ 8 - 1
app/build.gradle

@@ -14,7 +14,14 @@ android {
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
-
+    sourceSets {
+        main {
+            jniLibs.srcDirs 'src/main/libs'
+            assets.srcDirs = ['src/main/assets']
+            jni.srcDirs = []
+            java.srcDirs = ['src/main/java']
+        }
+    }
     buildTypes {
         release {
             minifyEnabled false

+ 15 - 1
app/src/main/AndroidManifest.xml

@@ -2,6 +2,10 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     package="com.xugame.gameconsole">
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.VIBRATE" />
 
     <application
         android:allowBackup="true"
@@ -22,7 +26,17 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name=".emulator.RetroArchEmulatorActivity"></activity>
+        <activity
+            android:name=".emulator.RetroArchEmulatorActivity"
+            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
+            android:launchMode="singleInstance">
+            <meta-data
+                android:name="android.app.lib_name"
+                android:value="retroarch-activity" />
+            <meta-data
+                android:name="android.app.func_name"
+                android:value="ANativeActivity_onCreate" />
+        </activity>
     </application>
 
 </manifest>

+ 12 - 0
app/src/main/java/com/xugame/BuildConfig.java

@@ -0,0 +1,12 @@
+package com.xugame;
+
+public class BuildConfig {
+    public static final boolean DEBUG = Boolean.parseBoolean("true");
+    public static final String APPLICATION_ID = "com.xugame.gameconsole";
+    public static final String BUILD_TYPE = "debug";
+    public static final String FLAVOR = "aarch64";
+    public static final int VERSION_CODE = 1685701716;
+    public static final String VERSION_NAME = "1.15.0_GIT";
+    // Field from product flavor: aarch64
+    public static final boolean PLAY_STORE_BUILD = false;
+}

+ 198 - 11
app/src/main/java/com/xugame/gameconsole/MainActivity.java

@@ -2,24 +2,197 @@ package com.xugame.gameconsole;
 
 import androidx.appcompat.app.AppCompatActivity;
 
+import android.Manifest;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.media.AudioManager;
 import android.os.Bundle;
+import android.os.Environment;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.util.Log;
 import android.view.View;
 import android.widget.Button;
 import android.widget.EditText;
 
 import com.xugame.gameconsole.emulator.RetroArchEmulatorActivity;
+import com.xugame.gameconsole.preferences.UserPreferences;
 
-public class MainActivity extends AppCompatActivity implements View.OnClickListener {
+import java.util.ArrayList;
+import java.util.List;
+
+public class MainActivity extends PreferenceActivity implements View.OnClickListener {
     private EditText mETRom, mETLib, mETConfig;
     private Button mBtnLocalGame, mBtnNetP1, mBtnNetP2;
 
+    final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
+    public static String PACKAGE_NAME;
+    boolean checkPermissions = false;
+
+    public void showMessageOKCancel(String message, DialogInterface.OnClickListener onClickListener)
+    {
+        new AlertDialog.Builder(this).setMessage(message)
+                .setPositiveButton("OK", onClickListener).setCancelable(false)
+                .setNegativeButton("Cancel", null).create().show();
+    }
+
+    private boolean addPermission(List<String> permissionsList, String permission)
+    {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M)
+        {
+            if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED)
+            {
+                permissionsList.add(permission);
+
+                // Check for Rationale Option
+                if (!shouldShowRequestPermissionRationale(permission))
+                    return false;
+            }
+        }
+
+        return true;
+    }
+
+    public void checkRuntimePermissions()
+    {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M)
+        {
+            // Android 6.0+ needs runtime permission checks
+            List<String> permissionsNeeded = new ArrayList<String>();
+            final List<String> permissionsList = new ArrayList<String>();
+
+            if (!addPermission(permissionsList, Manifest.permission.READ_EXTERNAL_STORAGE))
+                permissionsNeeded.add("Read External Storage");
+            if (!addPermission(permissionsList, Manifest.permission.WRITE_EXTERNAL_STORAGE))
+                permissionsNeeded.add("Write External Storage");
+
+            if (permissionsList.size() > 0)
+            {
+                checkPermissions = true;
+
+                if (permissionsNeeded.size() > 0)
+                {
+                    // Need Rationale
+                    Log.i("MainMenuActivity", "Need to request external storage permissions.");
+
+                    String message = "You need to grant access to " + permissionsNeeded.get(0);
+
+                    for (int i = 1; i < permissionsNeeded.size(); i++)
+                        message = message + ", " + permissionsNeeded.get(i);
+
+                    showMessageOKCancel(message,
+                            new DialogInterface.OnClickListener()
+                            {
+                                @Override
+                                public void onClick(DialogInterface dialog, int which)
+                                {
+                                    if (which == AlertDialog.BUTTON_POSITIVE)
+                                    {
+                                        requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
+                                                REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
+
+                                        Log.i("MainMenuActivity", "User accepted request for external storage permissions.");
+                                    }
+                                }
+                            });
+                }
+                else
+                {
+                    requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
+                            REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
+
+                    Log.i("MainMenuActivity", "Requested external storage permissions.");
+                }
+            }
+        }
+
+        if (!checkPermissions)
+        {
+            finalStartup();
+        }
+    }
+
+    public void finalStartup()
+    {
+        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+        Intent retro = new Intent(this, RetroArchEmulatorActivity.class);
+
+        retro.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+        startRetroActivity(
+                retro,
+                null,
+                prefs.getString("libretro_path", getApplicationInfo().dataDir + "/cores/"),
+                UserPreferences.getDefaultConfigPath(this),
+                Settings.Secure.getString(getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD),
+                getApplicationInfo().dataDir,
+                getApplicationInfo().sourceDir);
+        startActivity(retro);
+        finish();
+    }
+    public static void startRetroActivity(Intent retro, String contentPath, String corePath,
+                                          String configFilePath, String imePath, String dataDirPath, String dataSourcePath)
+    {
+        if (contentPath != null) {
+            retro.putExtra("ROM", contentPath);
+        }
+        retro.putExtra("LIBRETRO", corePath);
+        retro.putExtra("CONFIGFILE", configFilePath);
+        retro.putExtra("IME", imePath);
+        retro.putExtra("DATADIR", dataDirPath);
+        retro.putExtra("APK", dataSourcePath);
+        retro.putExtra("SDCARD", Environment.getExternalStorageDirectory().getAbsolutePath());
+        String external = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + PACKAGE_NAME + "/files";
+        retro.putExtra("EXTERNAL", external);
+        Log.i("TEST_STAT","corePath="+corePath+"\n"+"configFilePath="+configFilePath+"\nimePath="+imePath
+                +"\nDATADIR="+dataDirPath+"\nAPK="+dataSourcePath+
+                "\nSDCARD="+Environment.getExternalStorageDirectory().getAbsolutePath()+"\nEXTERNAL="
+                +external);
+    }
 
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
+    {
+        switch (requestCode)
+        {
+            case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
+                for (int i = 0; i < permissions.length; i++)
+                {
+                    if(grantResults[i] == PackageManager.PERMISSION_GRANTED)
+                    {
+                        Log.i("MainMenuActivity", "Permission: " + permissions[i] + " was granted.");
+                    }
+                    else
+                    {
+                        Log.i("MainMenuActivity", "Permission: " + permissions[i] + " was not granted.");
+                    }
+                }
+
+                break;
+            default:
+                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+                break;
+        }
+
+        finalStartup();
+    }
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_main);
-        this.findView();
+//        setContentView(R.layout.activity_main);
+//        this.findView();
+        PACKAGE_NAME = getPackageName();
+
+        // Bind audio stream to hardware controls.
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        UserPreferences.updateConfigFile(this);
+
+        checkRuntimePermissions();
     }
 
     private void findView() {
@@ -37,15 +210,29 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
 
     @Override
     public void onClick(View view) {
+
         if (view == mBtnLocalGame) {
-            Intent intent = new Intent(this, RetroArchEmulatorActivity.class);
-            String romPath = mETRom.getText().toString();
-            String libPath = mETLib.getText().toString();
-            String configPath = mETConfig.getText().toString();
-            intent.putExtra("rom_path", romPath);
-            intent.putExtra("lib_path", libPath);
-            intent.putExtra("config_path", configPath);
-            startActivity(intent);
+
+            if (!checkPermissions)
+            {
+            finalStartup();
+            }
+
+//            String PACKAGE_NAME = this.getPackageName();
+//            Intent retro = new Intent(this, RetroArchEmulatorActivity.class);
+//            retro.putExtra("ROM", "/storage/emulated/0/arcade/rom/kofxi.zip");
+//            retro.putExtra("LIBRETRO", "/data/data/com.xugame.gameconsole/cores/flycast_libretro_android.so");
+////            retro.putExtra("ROM", "");
+////            retro.putExtra("LIBRETRO", "");
+//
+//            retro.putExtra("CONFIGFILE", "/storage/emulated/0/Android/data/com.xugame.gameconsole/files/retroarch.cfg");
+//            retro.putExtra("IME", "com.android.inputmethod.latin/.LatinIME");
+//            retro.putExtra("DATADIR", "/data/user/0/com.xugame.gameconsole");
+//            retro.putExtra("APK", "/data/app/com.xugame.gameconsole-1/base.apk");
+//            retro.putExtra("SDCARD", Environment.getExternalStorageDirectory().getAbsolutePath());
+//            String external = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + PACKAGE_NAME + "/files";
+//            retro.putExtra("EXTERNAL", external);
+//            startActivity(retro);
         }
 
     }

+ 606 - 0
app/src/main/java/com/xugame/gameconsole/emulator/BaseRetroArchEmulator.java

@@ -0,0 +1,606 @@
+package com.xugame.gameconsole.emulator;
+
+import android.annotation.TargetApi;
+import android.app.NativeActivity;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.media.AudioAttributes;
+import android.os.BatteryManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.system.Os;
+import android.util.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import com.xugame.BuildConfig;
+import com.xugame.gameconsole.playcore.PlayCoreManager;
+import com.xugame.gameconsole.preferences.UserPreferences;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+
+public class BaseRetroArchEmulator extends NativeActivity {
+
+
+    static {
+        System.loadLibrary("retroarch-activity");
+    }
+
+    public static int FRONTEND_POWERSTATE_NONE = 0;
+    public static int FRONTEND_POWERSTATE_NO_SOURCE = 1;
+    public static int FRONTEND_POWERSTATE_CHARGING = 2;
+    public static int FRONTEND_POWERSTATE_CHARGED = 3;
+    public static int FRONTEND_POWERSTATE_ON_POWER_SOURCE = 4;
+    public static int FRONTEND_ORIENTATION_0 = 0;
+    public static int FRONTEND_ORIENTATION_90 = 1;
+    public static int FRONTEND_ORIENTATION_180 = 2;
+    public static int FRONTEND_ORIENTATION_270 = 3;
+    public static int RETRO_RUMBLE_STRONG = 0;
+    public static int RETRO_RUMBLE_WEAK = 1;
+    public boolean sustainedPerformanceMode = true;
+    public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        cleanupSymlinks();
+        updateSymlinks();
+
+        PlayCoreManager.getInstance().onCreate(this);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected void onDestroy() {
+        PlayCoreManager.getInstance().onDestroy();
+        super.onDestroy();
+    }
+
+    public void doVibrate(int id, int effect, int strength, int oneShot)
+    {
+        Vibrator vibrator = null;
+        int repeat = 0;
+        long[] pattern = {0, 16};
+        int[] strengths = {0, strength};
+
+        if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+            if (id == -1)
+                vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
+            else
+            {
+                InputDevice dev = InputDevice.getDevice(id);
+
+                if (dev != null)
+                    vibrator = dev.getVibrator();
+            }
+        }
+
+        if (vibrator == null)
+            return;
+
+        if (strength == 0) {
+            vibrator.cancel();
+            return;
+        }
+
+        if (oneShot > 0)
+            repeat = -1;
+        else
+            pattern[1] = 1000;
+
+        if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            if (id >= 0)
+                Log.i("RetroActivity", "Vibrate id " + id + ": strength " + strength);
+
+            vibrator.vibrate(VibrationEffect.createWaveform(pattern, strengths, repeat), new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_GAME).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build());
+        }else{
+            vibrator.vibrate(pattern, repeat);
+        }
+    }
+
+    public void doHapticFeedback(int effect)
+    {
+        getWindow().getDecorView().performHapticFeedback(effect,
+                HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+        Log.i("RetroActivity", "Haptic Feedback effect " + effect);
+    }
+
+    // Exiting cleanly from NDK seems to be nearly impossible.
+    // Have to use exit(0) to avoid weird things happening, even with runOnUiThread() approaches.
+    // Use a separate JNI function to explicitly trigger the readback.
+    public void onRetroArchExit()
+    {
+        finish();
+    }
+
+    public int getVolumeCount()
+    {
+        int ret = 0;
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            StorageManager storageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
+            List<StorageVolume> storageVolumeList = storageManager.getStorageVolumes();
+
+            for (int i = 0; i < storageVolumeList.size(); i++) {
+                ret++;
+            }
+            Log.i("RetroActivity", "volume count: " + ret);
+        }
+
+        return (int)ret;
+    }
+
+    public String getVolumePath(String input)
+    {
+        String ret = "";
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            int index = Integer.valueOf(input);
+            int j = 0;
+
+            StorageManager storageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
+            List<StorageVolume> storageVolumeList = storageManager.getStorageVolumes();
+
+            for (int i = 0; i < storageVolumeList.size(); i++) {
+                if (i == j) {
+                    ret = String.valueOf(storageVolumeList.get(index).getDirectory());
+                }
+            }
+            Log.i("RetroActivity", "volume path: " + ret);
+        }
+
+        return ret;
+    }
+
+    // https://stackoverflow.com/questions/4553650/how-to-check-device-natural-default-orientation-on-android-i-e-get-landscape/4555528#4555528
+    public int getDeviceDefaultOrientation() {
+        WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
+        Configuration config = getResources().getConfiguration();
+        int rotation = windowManager.getDefaultDisplay().getRotation();
+
+        if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) &&
+                config.orientation == Configuration.ORIENTATION_LANDSCAPE)
+                || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) &&
+                config.orientation == Configuration.ORIENTATION_PORTRAIT))
+        {
+            return Configuration.ORIENTATION_LANDSCAPE;
+        }else{
+            return Configuration.ORIENTATION_PORTRAIT;
+        }
+    }
+
+    public void setScreenOrientation(int orientation)
+    {
+        int naturalOrientation = getDeviceDefaultOrientation();
+        int newOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+        // We assume no device has a natural orientation that is reversed
+        switch (naturalOrientation) {
+            case Configuration.ORIENTATION_PORTRAIT:
+            {
+                if (orientation == FRONTEND_ORIENTATION_0) {
+                    newOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+                }else if (orientation == FRONTEND_ORIENTATION_90) {
+                    newOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+                }else if (orientation == FRONTEND_ORIENTATION_180) {
+                    newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+                }else if (orientation == FRONTEND_ORIENTATION_270) {
+                    newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+                }
+                break;
+            }
+            case Configuration.ORIENTATION_LANDSCAPE:
+            {
+                if (orientation == FRONTEND_ORIENTATION_0) {
+                    newOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+                }else if (orientation == FRONTEND_ORIENTATION_90) {
+                    newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+                }else if (orientation == FRONTEND_ORIENTATION_180) {
+                    newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+                }else if (orientation == FRONTEND_ORIENTATION_270) {
+                    newOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+                }
+                break;
+            }
+        }
+
+        screenOrientation = newOrientation;
+
+        Log.i("RetroActivity", "setting new orientation to " + screenOrientation);
+
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                setRequestedOrientation(screenOrientation);
+            }
+        });
+    }
+
+    public String getUserLanguageString()
+    {
+        String lang = Locale.getDefault().getLanguage();
+        String country = Locale.getDefault().getCountry();
+
+        if (lang.length() == 0)
+            return "en";
+
+        if (country.length() == 0)
+            return lang;
+
+        return lang + '_' + country;
+    }
+
+    @TargetApi(24)
+    public void setSustainedPerformanceMode(boolean on)
+    {
+        sustainedPerformanceMode = on;
+
+        if (Build.VERSION.SDK_INT >= 24) {
+            if (isSustainedPerformanceModeSupported()) {
+                final CountDownLatch latch = new CountDownLatch(1);
+
+                runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        Log.i("RetroActivity", "setting sustained performance mode to " + sustainedPerformanceMode);
+
+                        getWindow().setSustainedPerformanceMode(sustainedPerformanceMode);
+
+                        latch.countDown();
+                    }
+                });
+
+                try {
+                    latch.await();
+                }catch(InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    @TargetApi(24)
+    public boolean isSustainedPerformanceModeSupported()
+    {
+        boolean supported = false;
+
+        if (Build.VERSION.SDK_INT >= 24)
+        {
+            PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
+
+            if (powerManager.isSustainedPerformanceModeSupported())
+                supported = true;
+        }
+
+        Log.i("RetroActivity", "isSustainedPerformanceModeSupported? " + supported);
+
+        return supported;
+    }
+
+    public int getBatteryLevel()
+    {
+        IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+        // This doesn't actually register anything (or need to) because we know this particular intent is sticky and we do not specify a BroadcastReceiver anyway
+        Intent batteryStatus = registerReceiver(null, ifilter);
+        int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+        int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
+
+        float percent = ((float)level / (float)scale) * 100.0f;
+
+        Log.i("RetroActivity", "battery: level = " + level + ", scale = " + scale + ", percent = " + percent);
+
+        return (int)percent;
+    }
+
+    public int getPowerstate()
+    {
+        IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+        // This doesn't actually register anything (or need to) because we know this particular intent is sticky and we do not specify a BroadcastReceiver anyway
+        Intent batteryStatus = registerReceiver(null, ifilter);
+        int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+        boolean hasBattery = batteryStatus.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false);
+        boolean isCharging = (status == BatteryManager.BATTERY_STATUS_CHARGING);
+        boolean isCharged = (status == BatteryManager.BATTERY_STATUS_FULL);
+        int powerstate = FRONTEND_POWERSTATE_NONE;
+
+        if (isCharged)
+            powerstate = FRONTEND_POWERSTATE_CHARGED;
+        else if (isCharging)
+            powerstate = FRONTEND_POWERSTATE_CHARGING;
+        else if (!hasBattery)
+            powerstate = FRONTEND_POWERSTATE_NO_SOURCE;
+        else
+            powerstate = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
+
+        Log.i("RetroActivity", "power state = " + powerstate);
+
+        return powerstate;
+    }
+
+    public boolean isAndroidTV()
+    {
+        Configuration config = getResources().getConfiguration();
+        UiModeManager uiModeManager = (UiModeManager)getSystemService(UI_MODE_SERVICE);
+
+        if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
+        {
+            Log.i("RetroActivity", "isAndroidTV == true");
+            return true;
+        }
+        else
+        {
+            Log.i("RetroActivity", "isAndroidTV == false");
+            return false;
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        int oldOrientation = 0;
+        boolean hasOldOrientation = false;
+
+        super.onConfigurationChanged(newConfig);
+
+        Log.i("RetroActivity", "onConfigurationChanged: orientation is now " + newConfig.orientation);
+
+        SharedPreferences prefs = UserPreferences.getPreferences(this);
+        SharedPreferences.Editor edit = prefs.edit();
+
+        hasOldOrientation = prefs.contains("ORIENTATION");
+
+        if (hasOldOrientation)
+            oldOrientation = prefs.getInt("ORIENTATION", 0);
+
+        edit.putInt("ORIENTATION", newConfig.orientation);
+        edit.apply();
+
+        Log.i("RetroActivity", "hasOldOrientation? " + hasOldOrientation + " newOrientation: " + newConfig.orientation + " oldOrientation: " + oldOrientation);
+    }
+
+    /**
+     * Checks if this version of RetroArch is a Play Store build.
+     *
+     * @return true if this is a Play Store build, false otherwise
+     */
+    public boolean isPlayStoreBuild() {
+        Log.i("RetroActivity", "isPlayStoreBuild: " + BuildConfig.PLAY_STORE_BUILD);
+
+        return BuildConfig.PLAY_STORE_BUILD;
+    }
+
+    /**
+     * Gets the list of available cores that can be downloaded as Dynamic Feature Modules.
+     *
+     * @return the list of available cores
+     */
+    public String[] getAvailableCores() {
+        int id = getResources().getIdentifier("module_names_" + Build.CPU_ABI.replace('-', '_'), "array", getPackageName());
+
+        String[] returnVal = getResources().getStringArray(id);
+        Log.i("RetroActivity", "getAvailableCores: " + Arrays.toString(returnVal));
+        return returnVal;
+    }
+
+    /**
+     * Gets the list of cores that are currently installed as Dynamic Feature Modules.
+     *
+     * @return the list of installed cores
+     */
+    public String[] getInstalledCores() {
+        String[] modules = PlayCoreManager.getInstance().getInstalledModules();
+        List<String> cores = new ArrayList<>();
+        List<String> availableCores = Arrays.asList(getAvailableCores());
+
+        SharedPreferences prefs = UserPreferences.getPreferences(this);
+
+        for(int i = 0; i < modules.length; i++) {
+            String coreName = unsanitizeCoreName(modules[i]);
+            if(!prefs.getBoolean("core_deleted_" + coreName, false)
+                    && availableCores.contains(coreName)) {
+                cores.add(coreName);
+            }
+        }
+
+        String[] returnVal = cores.toArray(new String[0]);
+        Log.i("RetroActivity", "getInstalledCores: " + Arrays.toString(returnVal));
+        return returnVal;
+    }
+
+    /**
+     * Asks the system to download a core.
+     *
+     * @param coreName Name of the core to install
+     */
+    public void downloadCore(final String coreName) {
+        Log.i("RetroActivity", "downloadCore: " + coreName);
+
+        SharedPreferences prefs = UserPreferences.getPreferences(this);
+        prefs.edit().remove("core_deleted_" + coreName).apply();
+
+        PlayCoreManager.getInstance().downloadCore(coreName);
+    }
+
+    /**
+     * Asks the system to delete a core.
+     *
+     * Note that the actual module deletion will not happen immediately (the OS will delete
+     * it whenever it feels like it), but the symlink will still be immediately removed.
+     *
+     * @param coreName Name of the core to delete
+     */
+    public void deleteCore(String coreName) {
+        Log.i("RetroActivity", "deleteCore: " + coreName);
+
+        String newFilename = getCorePath() + coreName + "_libretro_android.so";
+        new File(newFilename).delete();
+
+        SharedPreferences prefs = UserPreferences.getPreferences(this);
+        prefs.edit().putBoolean("core_deleted_" + coreName, true).apply();
+
+        PlayCoreManager.getInstance().deleteCore(coreName);
+    }
+
+
+
+    /////////////// JNI methods ///////////////
+
+
+
+    /**
+     * Called when a core install is initiated.
+     *
+     * @param coreName Name of the core that the install is initiated for.
+     * @param successful true if success, false if failure
+     */
+    public native void coreInstallInitiated(String coreName, boolean successful);
+
+    /**
+     * Called when the status of a core install has changed.
+     *
+     * @param coreNames Names of all cores that are currently being downloaded.
+     * @param status One of INSTALL_STATUS_DOWNLOADING, INSTALL_STATUS_INSTALLING,
+     *               INSTALL_STATUS_INSTALLED, or INSTALL_STATUS_FAILED
+     * @param bytesDownloaded Number of bytes downloaded.
+     * @param totalBytesToDownload Total number of bytes to download.
+     */
+    public native void coreInstallStatusChanged(String[] coreNames, int status, long bytesDownloaded, long totalBytesToDownload);
+
+
+
+    /////////////// Private methods ///////////////
+
+
+
+    /**
+     * Sanitizes a core name so that it can be used when dealing with
+     * Dynamic Feature Modules. Needed because Gradle modules cannot use
+     * dashes, but we have at least one core name ("mesen-s") that uses them.
+     *
+     * @param coreName Name of the core to sanitize.
+     * @return The sanitized core name.
+     */
+    public String sanitizeCoreName(String coreName) {
+        return "core_" + coreName.replace('-', '_');
+    }
+
+    /**
+     * Unsanitizes a core name from its module name.
+     *
+     * @param coreName Name of the core to unsanitize.
+     * @return The unsanitized core name.
+     */
+    public String unsanitizeCoreName(String coreName) {
+        if(coreName.equals("core_mesen_s")) {
+            return "mesen-s";
+        }
+
+        return coreName.substring(5);
+    }
+
+    /**
+     * Gets the path to the RetroArch cores directory.
+     *
+     * @return The path to the RetroArch cores directory
+     */
+    private String getCorePath() {
+        String path = getApplicationInfo().dataDir + "/cores/";
+        new File(path).mkdirs();
+
+        return path;
+    }
+
+    /**
+     * Cleans up existing symlinks before new ones are created.
+     */
+    private void cleanupSymlinks() {
+        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
+
+        File[] files = new File(getCorePath()).listFiles();
+        for(int i = 0; i < files.length; i++) {
+            try {
+                Os.readlink(files[i].getAbsolutePath());
+                files[i].delete();
+            } catch (Exception e) {
+                // File is not a symlink, so don't delete.
+            }
+        }
+    }
+
+    /**
+     * Triggers a symlink update in the known places that Dynamic Feature Modules
+     * are installed to.
+     */
+    public void updateSymlinks() {
+        if(!isPlayStoreBuild()) return;
+
+        traverseFilesystem(getFilesDir());
+        traverseFilesystem(new File(getApplicationInfo().nativeLibraryDir));
+    }
+
+    /**
+     * Traverse the filesystem, looking for native libraries.
+     * Symlinks any libraries it finds to the main RetroArch "cores" folder,
+     * updating any existing symlinks with the correct path to the native libraries.
+     *
+     * This is necessary because Dynamic Feature Modules are first downloaded
+     * and installed to a temporary location on disk, before being moved
+     * to a more permanent location by the system at a later point.
+     *
+     * This could probably be done in native code instead, if that's preferred.
+     *
+     * @param file The parent directory of the tree to traverse.
+     */
+    private void traverseFilesystem(File file) {
+        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
+
+        File[] list = file.listFiles();
+        if(list == null) return;
+
+        List<String> availableCores = Arrays.asList(getAvailableCores());
+
+        // Check each file in a directory to see if it's a native library.
+        for(int i = 0; i < list.length; i++) {
+            File child = list[i];
+            String name = child.getName();
+
+            if(name.startsWith("lib") && name.endsWith(".so") && !name.contains("retroarch-activity")) {
+                // Found a native library!
+                String core = name.subSequence(3, name.length() - 3).toString();
+                String filename = child.getAbsolutePath();
+
+                SharedPreferences prefs = UserPreferences.getPreferences(this);
+                if(!prefs.getBoolean("core_deleted_" + core, false)
+                        && availableCores.contains(core)) {
+                    // Generate the destination filename and delete any existing symlinks / cores
+                    String newFilename = getCorePath() + core + "_libretro_android.so";
+                    new File(newFilename).delete();
+
+                    try {
+                        Os.symlink(filename, newFilename);
+                    } catch (Exception e) {
+                        // Symlink failed to be created. Should never happen.
+                    }
+                }
+            } else if(file.isDirectory()) {
+                // Found another directory, so traverse it
+                traverseFilesystem(child);
+            }
+        }
+    }
+}

+ 218 - 0
app/src/main/java/com/xugame/gameconsole/emulator/RetroActivityCamera.java

@@ -0,0 +1,218 @@
+package com.xugame.gameconsole.emulator;
+
+import android.content.SharedPreferences;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.xugame.gameconsole.preferences.UserPreferences;
+
+import java.io.IOException;
+
+public class RetroActivityCamera extends BaseRetroArchEmulator{
+
+    private Camera mCamera = null;
+    private long lastTimestamp = 0;
+    private SurfaceTexture texture;
+    private boolean updateSurface = true;
+    private boolean camera_service_running = false;
+
+    /**
+     * Executed when the {@link Camera}
+     * is staring to capture.
+     */
+    public void onCameraStart()
+    {
+        if (camera_service_running)
+            return;
+
+        if (mCamera != null)
+            mCamera.startPreview();
+        camera_service_running = true;
+    }
+
+    /**
+     * Executed when the {@link Camera} is done capturing.
+     * <p>
+     * Note that this does not release the currently held
+     * {@link Camera} instance and must be freed by calling
+     * {@link #onCameraFree}
+     */
+    public void onCameraStop()
+    {
+        if (!camera_service_running)
+            return;
+
+        if (mCamera != null)
+            mCamera.stopPreview();
+        camera_service_running = false;
+    }
+
+    /**
+     * Releases the currently held {@link Camera} instance.
+     */
+    public void onCameraFree()
+    {
+        onCameraStop();
+
+        if (mCamera != null)
+            mCamera.release();
+    }
+
+    /**
+     * Initializes the camera for use.
+     */
+    public void onCameraInit()
+    {
+        if (mCamera != null)
+            return;
+
+        mCamera = Camera.open();
+    }
+
+    /**
+     * Polls the camera for updates to the {@link SurfaceTexture}.
+     *
+     * @return true if polling was successful, false otherwise.
+     */
+    public boolean onCameraPoll()
+    {
+        if (!camera_service_running)
+            return false;
+
+        if (texture == null)
+        {
+            Log.i("RetroActivity", "No texture");
+            return true;
+        }
+        else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+        {
+            if (updateSurface)
+            {
+                texture.updateTexImage();
+            }
+
+            long newTimestamp = texture.getTimestamp();
+
+            if (newTimestamp != lastTimestamp)
+            {
+                lastTimestamp = newTimestamp;
+                return true;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Initializes the {@link SurfaceTexture} used by the
+     * {@link Camera} with a given OpenGL texure ID.
+     *
+     * @param gl_texid texture ID to initialize the
+     *        {@link SurfaceTexture} with.
+     */
+    public void onCameraTextureInit(int gl_texid)
+    {
+        texture = new SurfaceTexture(gl_texid);
+        texture.setOnFrameAvailableListener(onCameraFrameAvailableListener);
+    }
+
+    /**
+     * Sets the {@link Camera} texture with the texture represented
+     * by the given OpenGL texture ID.
+     *
+     * @param gl_texid     The texture ID representing the texture to set the camera to.
+     * @throws IOException If setting the texture fails.
+     */
+    public void onCameraSetTexture(int gl_texid) throws IOException
+    {
+        if (texture == null)
+            onCameraTextureInit(gl_texid);
+
+        if (mCamera != null)
+            mCamera.setPreviewTexture(texture);
+    }
+
+    private final SurfaceTexture.OnFrameAvailableListener onCameraFrameAvailableListener = new SurfaceTexture.OnFrameAvailableListener()
+    {
+        @Override
+        public void onFrameAvailable(SurfaceTexture surfaceTexture)
+        {
+            updateSurface = true;
+        }
+    };
+
+    @Override
+    public void onCreate(Bundle savedInstanceState)
+    {
+        // Save the current setting for updates
+        SharedPreferences prefs = UserPreferences.getPreferences(this);
+        SharedPreferences.Editor edit = prefs.edit();
+        edit.putBoolean("CAMERA_UPDATES_ON", false);
+        edit.apply();
+
+        camera_service_running = false;
+
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    public void onPause()
+    {
+        // Save the current setting for updates
+        SharedPreferences prefs = UserPreferences.getPreferences(this);
+        SharedPreferences.Editor edit = prefs.edit();
+        edit.putBoolean("CAMERA_UPDATES_ON", camera_service_running);
+        edit.apply();
+
+        onCameraStop();
+        super.onPause();
+    }
+
+    @Override
+    public void onResume()
+    {
+        SharedPreferences prefs = UserPreferences.getPreferences(this);
+        SharedPreferences.Editor edit = prefs.edit();
+
+        /*
+         * Get any previous setting for camera updates
+         * Gets "false" if an error occurs
+         */
+        if (prefs.contains("CAMERA_UPDATES_ON"))
+        {
+            camera_service_running = prefs.getBoolean("CAMERA_UPDATES_ON", false);
+            if (camera_service_running)
+            {
+                onCameraStart();
+            }
+        }
+        else // Otherwise, turn off camera updates
+        {
+            edit.putBoolean("CAMERA_UPDATES_ON", false);
+            edit.apply();
+            camera_service_running = false;
+        }
+
+        super.onResume();
+    }
+
+    @Override
+    public void onDestroy()
+    {
+        onCameraFree();
+        super.onDestroy();
+    }
+
+    @Override
+    public void onStop()
+    {
+        onCameraStop();
+        super.onStop();
+    }
+
+}

+ 104 - 27
app/src/main/java/com/xugame/gameconsole/emulator/RetroArchEmulatorActivity.java

@@ -2,46 +2,123 @@ package com.xugame.gameconsole.emulator;
 
 import android.app.Activity;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
+import android.hardware.input.InputManager;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
 
 import androidx.annotation.Nullable;
 
+import com.xugame.gameconsole.MainActivity;
 import com.xugame.gameconsole.R;
+import com.xugame.gameconsole.preferences.ConfigFile;
+import com.xugame.gameconsole.preferences.UserPreferences;
 
-public class RetroArchEmulatorActivity extends Activity {
-    private static final String TAG = "RetroArchEmulatorActivityTAG";
-    private String romPath, libsPath, configPath;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+//private void parserIntent() {
+//        Intent intent = getIntent();
+//        romPath = intent.getStringExtra("rom_path");
+//        romPath = intent.getStringExtra("rom_path");
+//        libsPath = intent.getStringExtra("lib_path");
+//        configPath = intent.getStringExtra("config_path");
+//        }
+public class RetroArchEmulatorActivity extends RetroActivityCamera {
+
+    // If set to true then Retroarch will completely exit when it loses focus
+    private boolean quitfocus = false;
 
     @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.retro_arch_layout);
-        parserIntent();
-        startGame();
+    public void onResume() {
+        super.onResume();
+
+        setSustainedPerformanceMode(sustainedPerformanceMode);
+
+        if (Build.VERSION.SDK_INT >= 19) {
+            // Immersive mode
+
+            // Constants from API > 14
+            final int API_SYSTEM_UI_FLAG_LAYOUT_STABLE = 0x00000100;
+            final int API_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 0x00000200;
+            final int API_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400;
+            final int API_SYSTEM_UI_FLAG_FULLSCREEN = 0x00000004;
+            final int API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000;
+
+            View thisView = getWindow().getDecorView();
+            thisView.setSystemUiVisibility(API_SYSTEM_UI_FLAG_LAYOUT_STABLE
+                    | API_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                    | API_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                    | API_SYSTEM_UI_FLAG_FULLSCREEN
+                    | API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+
+            // Check for Android UI specific parameters
+            Intent retro = getIntent();
+
+            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+                String refresh = retro.getStringExtra("REFRESH");
+
+                // If REFRESH parameter is provided then try to set refreshrate accordingly
+                if(refresh != null) {
+                    WindowManager.LayoutParams params = getWindow().getAttributes();
+                    params.preferredRefreshRate = Integer.parseInt(refresh);
+                    getWindow().setAttributes(params);
+                }
+            }
+
+            // If QUITFOCUS parameter is provided then enable that Retroarch quits when focus is lost
+            quitfocus = retro.hasExtra("QUITFOCUS");
+
+            // If HIDEMOUSE parameters is provided then hide the mourse cursor
+            // This requires NVIDIA Android extensions (available on NVIDIA Shield), if they are not
+            // available then nothing will be done
+            if (retro.hasExtra("HIDEMOUSE")) hideMouseCursor();
+        }
+
+        //Checks if Android versions is above 9.0 (28) and enable the screen to write over notch if the user desires
+        if (Build.VERSION.SDK_INT >= 28) {
+            ConfigFile configFile = new ConfigFile(UserPreferences.getDefaultConfigPath(this));
+            try {
+                if (configFile.getBoolean("video_notch_write_over_enable")) {
+                    getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+                }
+            } catch (Exception e) {
+                Log.w("Key doesn't exist yet.", e.getMessage());
+            }
+        }
     }
 
-    private void parserIntent() {
-        Intent intent = getIntent();
-        romPath = intent.getStringExtra("rom_path");
-        libsPath = intent.getStringExtra("lib_path");
-        configPath = intent.getStringExtra("config_path");
+    public void hideMouseCursor() {
+
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+            // Check for NVIDIA extensions and minimum SDK version
+            Method mInputManager_setCursorVisibility;
+            try { mInputManager_setCursorVisibility =
+                    InputManager.class.getMethod("setCursorVisibility", boolean.class);
+            }
+            catch (NoSuchMethodException ex) {
+                return; // Extensions were not available so do nothing
+            }
+
+            // Hide the mouse cursor
+            InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
+            try { mInputManager_setCursorVisibility.invoke(inputManager, false); }
+            catch (InvocationTargetException ite) { }
+            catch (IllegalAccessException iae)    { }
+        }
     }
 
-    private void startGame() {
-        Intent retro=new Intent();
-        retro.setComponent(new ComponentName("com.retroarch.aarch64",
-                "com.retroarch.browser.retroactivity.RetroActivityFuture"));
-        retro.putExtra("ROM", romPath);
-        retro.putExtra("LIBRETRO", libsPath);
-        retro.putExtra("CONFIGFILE", configPath);
-        retro.putExtra("IME", "com.android.inputmethod.latin/.LatinIME");
-        retro.putExtra("DATADIR", "/data/user/0/com.retroarch.aarch64");
-        retro.putExtra("APK", "/data/app/com.retroarch.aarch64-1/base.apk");
-        retro.putExtra("SDCARD", Environment.getExternalStorageDirectory().getAbsolutePath());
-        String external = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/com.retroarch.aarch64/files";
-        retro.putExtra("EXTERNAL", external);
-        startActivity(retro);
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        // If QUITFOCUS parameter was set then completely exit Retroarch when focus is lost
+        if (quitfocus) System.exit(0);
     }
 }

+ 28 - 0
app/src/main/java/com/xugame/gameconsole/playcore/PlayCoreManager.java

@@ -0,0 +1,28 @@
+package com.xugame.gameconsole.playcore;
+
+import com.xugame.gameconsole.emulator.BaseRetroArchEmulator;
+
+public class PlayCoreManager {
+
+    private PlayCoreManager() {}
+
+    private static PlayCoreManager instance;
+
+    public static PlayCoreManager getInstance() {
+        if (instance == null) {
+            instance = new PlayCoreManager();
+        }
+
+        return instance;
+    }
+
+    public void onCreate(BaseRetroArchEmulator newActivity) {}
+    public void onDestroy() {}
+
+    public String[] getInstalledModules() {
+        return new String[0];
+    }
+
+    public void downloadCore(final String coreName) {}
+    public void deleteCore(String coreName) {}
+}

+ 281 - 0
app/src/main/java/com/xugame/gameconsole/preferences/ConfigFile.java

@@ -0,0 +1,281 @@
+package com.xugame.gameconsole.preferences;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.util.Log;
+
+/**
+ * Represents a configuration file that works off of a key-value pair
+ * in the form [key name] = "[value]".
+ */
+public final class ConfigFile
+{
+	// Map containing all of the key-value pairs.
+	private final HashMap<String, String> map = new HashMap<String, String>();
+
+	/**
+	 * Constructor
+	 */
+	public ConfigFile()
+	{
+	}
+
+	/**
+	 * Constructor
+	 * 
+	 * @param filePath The path to the configuration file to open.
+	 */
+	public ConfigFile(String filePath)
+	{
+		if (filePath == null)
+			throw new IllegalArgumentException("filePath cannot be null.");
+
+		try 
+		{
+			open(filePath);
+		}
+		catch (IOException ioe)
+		{
+			Log.e("ConfigFile", "Stream reading the configuration file was suddenly closed for an unknown reason.");
+		}
+	}
+
+	/**
+	 * Parses a configuration file from the given stream
+	 * and appends the parsed values to the key-value map.
+	 * 
+	 * @param stream The {@link InputStream} containing the configuration file to parse.
+	 */
+	public void append(InputStream stream) throws IOException
+	{
+		BufferedReader br = new BufferedReader(new InputStreamReader(stream));
+
+		String line;
+		while ((line = br.readLine()) != null)
+			parseLine(line);
+
+		br.close();
+	}
+
+	/**
+	 * Opens a configuration file given by configPath
+	 * and parses all of its key-value pairs, adding
+	 * them to the key-value map.
+	 * 
+	 * @param configPath Path to the configuration file to parse.
+	 */
+	public void open(String configPath) throws IOException
+	{
+		clear();
+		append(new FileInputStream(configPath));
+	}
+
+	private void parseLine(String line)
+	{
+		String[] tokens = line.split("=", 2);
+		if (tokens.length < 2)
+			return;
+
+		for (int i = 0; i < tokens.length; i++)
+			tokens[i] = tokens[i].trim();
+
+		String key = tokens[0];
+		String value = tokens[1];
+
+		if (value.startsWith("\""))
+			value = value.substring(1, value.lastIndexOf('\"'));
+		else
+			value = value.split(" ")[0];
+
+		if (value.length() > 0)
+			map.put(key, value);
+	}
+
+	/**
+	 * Clears the key-value map of all currently set keys and values.
+	 */
+	public void clear()
+	{
+		map.clear();
+	}
+
+	/**
+	 * Writes the currently set key-value pairs to 
+	 * 
+	 * @param path         The path to save the 
+	 * 
+	 * @throws IOException
+	 */
+	public void write(String path) throws IOException
+	{
+		PrintWriter writer = new PrintWriter(path);
+
+		for (Map.Entry<String, String> entry : map.entrySet())
+		{
+			writer.println(entry.getKey() + " = \"" + entry.getValue() + "\"");
+		}
+
+		writer.close();
+	}
+
+	/**
+	 * Checks if a key exists in the {@link HashMap}
+	 * backing this ConfigFile instance.
+	 * 
+	 * @param key The key to check for.
+	 * 
+	 * @return true if the key exists in the HashMap backing
+	 *         this ConfigFile; false if it doesn't.
+	 */
+	public boolean keyExists(String key)
+	{
+		return map.containsKey(key);
+	}
+
+	/**
+	 * Sets a key to the given String value.
+	 * 
+	 * @param key   The key to set the String value to.
+	 * @param value The String value to set to the key.
+	 */
+	public void setString(String key, String value)
+	{
+		map.put(key, value);
+	}
+
+	/**
+	 * Sets a key to the given boolean value.
+	 * 
+	 * @param key   The key to set the boolean value to.
+	 * @param value The boolean value to set to the key.
+	 */
+	public void setBoolean(String key, boolean value)
+	{
+		map.put(key, Boolean.toString(value));
+	}
+
+	/**
+	 * Sets a key to the given Integer value.
+	 * 
+	 * @param key   The key to set the Integer value to.
+	 * @param value The Integer value to set to the key.
+	 */
+	public void setInt(String key, int value)
+	{
+		map.put(key, Integer.toString(value));
+	}
+
+	/**
+	 * Sets a key to the given double value.
+	 * 
+	 * @param key   The key to set the double value to.
+	 * @param value The double value to set to the key.
+	 */
+	public void setDouble(String key, double value)
+	{
+		map.put(key, Double.toString(value));
+	}
+	
+	/**
+	 * Sets a key to the given float value.
+	 * 
+	 * @param key   The key to set the float value to.
+	 * @param value The float value to set to the key.
+	 */
+	public void setFloat(String key, float value)
+	{
+		map.put(key, Float.toString(value));
+	}
+
+	/**
+	 * Gets the String value associated with the given key.
+	 * 
+	 * @param key The key to get the String value from.
+	 * 
+	 * @return the String object associated with the given key.
+	 */
+	public String getString(String key)
+	{
+		String ret = map.get(key);
+
+		if (ret != null)
+			return ret;
+		else
+			return null;
+	}
+
+	/**
+	 * Gets the Integer value associated with the given key.
+	 * 
+	 * @param key The key to get the Integer value from.
+	 * 
+	 * @return the Integer value associated with the given key.
+	 */
+	public int getInt(String key)
+	{
+		String str = getString(key);
+
+		if (str != null)
+			return Integer.parseInt(str);
+		else
+			throw new IllegalArgumentException("Config key '" + key + "' is invalid.");
+	}
+
+	/**
+	 * Gets the double value associated with the given key.
+	 * 
+	 * @param key The key to get the double value from.
+	 * 
+	 * @return the double value associated with the given key.
+	 */
+	public double getDouble(String key)
+	{
+		String str = getString(key);
+
+		if (str != null)
+			return Double.parseDouble(str);
+		else
+			throw new IllegalArgumentException("Config key '" + key + "' is invalid.");
+	}
+
+	/**
+	 * Gets the float value associated with the given key.
+	 * 
+	 * @param key The key to get the float value from.
+	 * 
+	 * @return the float value associated with the given key.
+	 */
+	public float getFloat(String key)
+	{
+		String str = getString(key);
+
+		if (str != null)
+			return Float.parseFloat(str);
+		else
+			throw new IllegalArgumentException("Config key '" + key + "' is invalid.");
+	}
+
+	/**
+	 * Gets the boolean value associated with the given key.
+	 * 
+	 * @param key The key to get the boolean value from.
+	 * 
+	 * @return the boolean value associated with the given key.
+	 */
+	public boolean getBoolean(String key)
+	{
+		String str = getString(key);
+
+		if (str != null)
+			return Boolean.parseBoolean(str);
+		else
+			throw new IllegalArgumentException("Config key '" + key + "' is invalid.");
+	}
+}

+ 313 - 0
app/src/main/java/com/xugame/gameconsole/preferences/UserPreferences.java

@@ -0,0 +1,313 @@
+package com.xugame.gameconsole.preferences;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+
+public class UserPreferences {
+
+    // Logging tag.
+    private static final String TAG = "UserPreferences";
+
+    // Disallow explicit instantiation.
+    private UserPreferences()
+    {
+    }
+
+    /**
+     * Retrieves the path to the default location of the libretro config.
+     *
+     * @param ctx the current {@link Context}
+     *
+     * @return the path to the default location of the libretro config.
+     */
+    public static String getDefaultConfigPath(Context ctx)
+    {
+        // Internal/External storage dirs.
+        final String internal = ctx.getFilesDir().getAbsolutePath();
+        String external = null;
+
+        // Get the App's external storage folder
+        final String state = android.os.Environment.getExternalStorageState();
+        if (android.os.Environment.MEDIA_MOUNTED.equals(state)) {
+            File extsd = ctx.getExternalFilesDir(null);
+            external = extsd.getAbsolutePath();
+        }
+
+        // Native library directory and data directory for this front-end.
+        final String dataDir = ctx.getApplicationInfo().dataDir;
+        final String coreDir = dataDir + "/cores/";
+
+        // Get libretro name and path
+        final SharedPreferences prefs = getPreferences(ctx);
+        final String libretro_path = prefs.getString("libretro_path", coreDir);
+
+        // Check if global config is being used. Return true upon failure.
+        final boolean globalConfigEnabled = prefs.getBoolean("global_config_enable", true);
+
+        String append_path;
+        // If we aren't using the global config.
+        if (!globalConfigEnabled && !libretro_path.equals(coreDir))
+        {
+            String sanitized_name = sanitizeLibretroPath(libretro_path);
+            append_path = File.separator + sanitized_name + ".cfg";
+        }
+        else // Using global config.
+        {
+            append_path = File.separator + "retroarch.cfg";
+        }
+
+        if (external != null)
+        {
+            String confPath = external + append_path;
+            if (new File(confPath).exists())
+                return confPath;
+        }
+        else if (internal != null)
+        {
+            String confPath = internal + append_path;
+            if (new File(confPath).exists())
+                return confPath;
+        }
+        else
+        {
+            String confPath = "/mnt/extsd" + append_path;
+            if (new File(confPath).exists())
+                return confPath;
+        }
+
+        // Config file does not exist. Create empty one.
+
+        // emergency fallback
+        String new_path = "/mnt/sd" + append_path;
+
+        if (external != null)
+            new_path = external + append_path;
+        else if (internal != null)
+            new_path = internal + append_path;
+        else if (dataDir != null)
+            new_path = dataDir + append_path;
+
+        try {
+            new File(new_path).createNewFile();
+        }
+        catch (IOException e)
+        {
+            Log.e(TAG, "Failed to create config file to: " + new_path);
+        }
+        return new_path;
+    }
+
+    /**
+     * Updates the libretro configuration file
+     * with new values if settings have changed.
+     *
+     * @param ctx the current {@link Context}.
+     */
+    public static void updateConfigFile(Context ctx)
+    {
+        String path = getDefaultConfigPath(ctx);
+        ConfigFile config = new ConfigFile(path);
+
+        Log.i(TAG, "Writing config to: " + path);
+
+        final String dataDir = ctx.getApplicationInfo().dataDir;
+        final String coreDir = dataDir + "/cores/";
+
+        final SharedPreferences prefs = getPreferences(ctx);
+
+        config.setString("libretro_directory", coreDir);
+
+        int samplingRate = getOptimalSamplingRate(ctx);
+        if (samplingRate != -1) {
+            config.setInt("audio_out_rate", samplingRate);
+        }
+
+        try
+        {
+            int version							= ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionCode;
+            final String dst_path			= dataDir;
+            final String dst_path_subdir	= "assets";
+
+            Log.i(TAG, "dst dir is: " + dst_path);
+            Log.i(TAG, "dst subdir is: " + dst_path_subdir);
+
+            config.setString("bundle_assets_src_path", ctx.getApplicationInfo().sourceDir);
+            config.setString("bundle_assets_dst_path", dst_path);
+            config.setString("bundle_assets_dst_path_subdir", dst_path_subdir);
+            config.setInt("bundle_assets_extract_version_current", version);
+        }
+        catch (PackageManager.NameNotFoundException ignored)
+        {
+        }
+
+        // Refactor this entire mess and make this usable for per-core config
+        if (Build.VERSION.SDK_INT >= 17 && prefs.getBoolean("audio_latency_auto", true))
+        {
+            int bufferSize = getLowLatencyBufferSize(ctx);
+            if (bufferSize != -1) {
+                config.setInt("audio_block_frames", bufferSize);
+            }
+        }
+
+        try
+        {
+            config.write(path);
+        }
+        catch (IOException e)
+        {
+            Log.e(TAG, "Failed to save config file to: " + path);
+        }
+    }
+
+    private static void readbackString(ConfigFile cfg, SharedPreferences.Editor edit, String key)
+    {
+        if (cfg.keyExists(key))
+            edit.putString(key, cfg.getString(key));
+        else
+            edit.remove(key);
+    }
+
+    private static void readbackBool(ConfigFile cfg, SharedPreferences.Editor edit, String key)
+    {
+        if (cfg.keyExists(key))
+            edit.putBoolean(key, cfg.getBoolean(key));
+        else
+            edit.remove(key);
+    }
+
+    private static void readbackDouble(ConfigFile cfg, SharedPreferences.Editor edit, String key)
+    {
+        if (cfg.keyExists(key))
+            edit.putFloat(key, (float)cfg.getDouble(key));
+        else
+            edit.remove(key);
+    }
+
+	/*
+	private static void readbackFloat(ConfigFile cfg, SharedPreferences.Editor edit, String key)
+	{
+		if (cfg.keyExists(key))
+			edit.putFloat(key, cfg.getFloat(key));
+		else
+			edit.remove(key);
+	}
+	*/
+
+    /**
+     private static void readbackInt(ConfigFile cfg, SharedPreferences.Editor edit, String key)
+     {
+     if (cfg.keyExists(key))
+     edit.putInt(key, cfg.getInt(key));
+     else
+     edit.remove(key);
+     }
+     */
+
+    /**
+     * Sanitizes a libretro core path.
+     *
+     * @param path The path to the libretro core.
+     *
+     * @return the sanitized libretro path.
+     */
+    private static String sanitizeLibretroPath(String path)
+    {
+        String sanitized_name = path.substring(
+                path.lastIndexOf('/') + 1,
+                path.lastIndexOf('.'));
+        sanitized_name = sanitized_name.replace("neon", "");
+        sanitized_name = sanitized_name.replace("libretro_", "");
+
+        return sanitized_name;
+    }
+
+    /**
+     * Gets a {@link SharedPreferences} instance containing current settings.
+     *
+     * @param ctx the current {@link Context}.
+     *
+     * @return A SharedPreference instance containing current settings.
+     */
+    public static SharedPreferences getPreferences(Context ctx)
+    {
+        return PreferenceManager.getDefaultSharedPreferences(ctx);
+    }
+
+    /**
+     * Gets the optimal sampling rate for low-latency audio playback.
+     *
+     * @param ctx the current {@link Context}.
+     *
+     * @return the optimal sampling rate for low-latency audio playback in Hz.
+     */
+    @TargetApi(17)
+    private static int getLowLatencyOptimalSamplingRate(Context ctx)
+    {
+        AudioManager manager = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE);
+        String value = manager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
+
+        if(value == null || value.isEmpty()) {
+            return -1;
+        }
+
+        return Integer.parseInt(value);
+    }
+
+    /**
+     * Gets the optimal buffer size for low-latency audio playback.
+     *
+     * @param ctx the current {@link Context}.
+     *
+     * @return the optimal output buffer size in decimal PCM frames.
+     */
+    @TargetApi(17)
+    private static int getLowLatencyBufferSize(Context ctx)
+    {
+        AudioManager manager = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE);
+        String value = manager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+
+        if(value == null || value.isEmpty()) {
+            return -1;
+        }
+
+        int buffersize = Integer.parseInt(value);
+        Log.i(TAG, "Queried ideal buffer size (frames): " + buffersize);
+        return buffersize;
+    }
+
+    /**
+     * Gets the optimal audio sampling rate.
+     * <p>
+     * On Android 4.2+ devices this will retrieve the optimal low-latency sampling rate,
+     * since Android 4.2 adds support for low latency audio in general.
+     * <p>
+     * On other devices, it simply returns the regular optimal sampling rate
+     * as returned by the hardware.
+     *
+     * @param ctx The current {@link Context}.
+     *
+     * @return the optimal audio sampling rate in Hz.
+     */
+    private static int getOptimalSamplingRate(Context ctx)
+    {
+        int ret;
+        if (Build.VERSION.SDK_INT >= 17)
+            ret = getLowLatencyOptimalSamplingRate(ctx);
+        else
+            ret = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
+
+        Log.i(TAG, "Using sampling rate: " + ret + " Hz");
+        return ret;
+    }
+
+}

+ 1 - 1
app/src/main/res/values/strings.xml

@@ -1,3 +1,3 @@
 <resources>
-    <string name="app_name">GameConsole</string>
+    <string name="app_name">测试掌机</string>
 </resources>