package com.retroarch.browser.retroactivity; import com.xugame.BuildConfig; import com.xugame.app.EnvironmentCallback; import com.xugame.app.EnvironmentCallbackCmd; import com.xugame.app.JoypadManager; import com.xugame.bean.InputDescriptorBean; import com.xugame.bean.SystemAVInfo; import com.xugame.gameconsole.playcore.PlayCoreManager; import com.xugame.gameconsole.preferences.UserPreferences; import com.xugame.gameconsole.util.DebugUtil; import android.annotation.TargetApi; import android.app.Activity; import android.app.NativeActivity; import android.content.res.Configuration; import android.content.Context; import android.content.SharedPreferences; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.media.AudioAttributes; import android.os.Bundle; import android.os.CountDownTimer; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.system.Os; import android.view.HapticFeedbackConstants; import android.view.InputDevice; import android.view.InputQueue; import android.view.Surface; import android.view.SurfaceHolder; import android.view.WindowManager; import android.app.UiModeManager; import android.os.BatteryManager; import android.os.Build; import android.os.PowerManager; import android.os.Vibrator; import android.os.VibrationEffect; import android.util.Log; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.Locale; /** * Class which provides common methods for RetroActivity related classes. */ //public class RetroActivityCommon extends NativeActivity public abstract class RetroActivityCommon extends NativeActivity { private static final String TAG = "RetroActivityCommonTAG"; 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; protected EnvironmentCallback callback = null; public abstract void showExitDialog(); public void environmentCallback(int cmd, Object data) { if (callback == null) return; Log.i(TAG, "environmentCallback"); if (cmd == EnvironmentCallbackCmd.SET_INPUT_DESCRIPTORS) { callback.setInputDescriptors((InputDescriptorBean[]) data); } else if (cmd == EnvironmentCallbackCmd.SET_SYSTEM_AV_INFO) { callback.setSystemAVInfo((SystemAVInfo) data); } else if (cmd == EnvironmentCallbackCmd.SET_GEOMETRY) { callback.setGeometry((SystemAVInfo.GameGeometry) data); } } @Override protected void onCreate(Bundle savedInstanceState) { cleanupSymlinks(); updateSymlinks(); PlayCoreManager.getInstance().onCreate(this); super.onCreate(savedInstanceState); // startCuntDown(); } private native void registerBeans(); private native void unregisterBeans(); @Override protected void onStart() { super.onStart(); registerBeans(); } @Override protected void onStop() { super.onStop(); } private CountDownTimer mDownTimer; private void startCuntDown(){ mDownTimer=new CountDownTimer(4000,1000) { @Override public void onTick(long millisUntilFinished) { DebugUtil.i(TAG,"onTick"+millisUntilFinished); } @Override public void onFinish() { finish(); } }; mDownTimer.start(); } @Override protected void onDestroy() { PlayCoreManager.getInstance().onDestroy(); unregisterBeans(); 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 >= 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 >= 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() { DebugUtil.i(TAG,"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 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 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; } } //call by jni public void openGameDialog() { DebugUtil.i(TAG,"openGameDialog"); showExitDialog(); } @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 cores = new ArrayList<>(); List 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 /////////////// /** * 点击返回时,退出库 */ public native void exitLocalGame(); /** * 保存状态 * @param stateIndex 状态索引 * @return 成功返回true,失败返回false */ public native boolean saveState(int stateIndex); /** * 加载状态 * @param stateIndex 状态索引 * @return 成功返回true,失败返回false */ public native boolean loadState(int stateIndex); /** * 设置分辨率 */ public native void setAspectRatio(int aspectRatioIndex); /** * 设置分辨率自定义时,的位置和大小 */ public native void setCustomX(int x); public native void setCustomY(int y); public native void setCustomWidth(int width); public native void setCustomHeight(int height); public native void setCustomViewPort(int x, int y, int width, int height); /** * 切换渲染路径 */ public native void switchFilter(String path); /** * 切换渲染路径 */ public native void setInputKeys(int port, int keyBindId, int key); /** * 保存配置 */ public native void saveOverrideConfig(); public native void gameDialogClosed(); public native void setJoystickBindNative(int port, int[] joyKeys, int[] joyAxiss); /////////////// 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 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); } } } }