RetroActivityCommon.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. package com.retroarch.browser.retroactivity;
  2. import com.xugame.BuildConfig;
  3. import com.xugame.gameconsole.playcore.PlayCoreManager;
  4. import com.xugame.gameconsole.preferences.UserPreferences;
  5. import com.xugame.gameconsole.util.DebugUtil;
  6. import android.annotation.TargetApi;
  7. import android.app.NativeActivity;
  8. import android.content.res.Configuration;
  9. import android.content.Context;
  10. import android.content.SharedPreferences;
  11. import android.content.Intent;
  12. import android.content.IntentFilter;
  13. import android.content.pm.ActivityInfo;
  14. import android.media.AudioAttributes;
  15. import android.os.Bundle;
  16. import android.os.storage.StorageManager;
  17. import android.os.storage.StorageVolume;
  18. import android.system.Os;
  19. import android.view.HapticFeedbackConstants;
  20. import android.view.InputDevice;
  21. import android.view.Surface;
  22. import android.view.WindowManager;
  23. import android.app.UiModeManager;
  24. import android.os.BatteryManager;
  25. import android.os.Build;
  26. import android.os.PowerManager;
  27. import android.os.Vibrator;
  28. import android.os.VibrationEffect;
  29. import android.util.Log;
  30. import java.io.File;
  31. import java.util.ArrayList;
  32. import java.util.Arrays;
  33. import java.util.List;
  34. import java.util.concurrent.CountDownLatch;
  35. import java.util.Locale;
  36. /**
  37. * Class which provides common methods for RetroActivity related classes.
  38. */
  39. public class RetroActivityCommon extends NativeActivity
  40. {
  41. private static final String TAG = "RetroActivityCommonTAG";
  42. static {
  43. System.loadLibrary("retroarch-activity");
  44. }
  45. public static int FRONTEND_POWERSTATE_NONE = 0;
  46. public static int FRONTEND_POWERSTATE_NO_SOURCE = 1;
  47. public static int FRONTEND_POWERSTATE_CHARGING = 2;
  48. public static int FRONTEND_POWERSTATE_CHARGED = 3;
  49. public static int FRONTEND_POWERSTATE_ON_POWER_SOURCE = 4;
  50. public static int FRONTEND_ORIENTATION_0 = 0;
  51. public static int FRONTEND_ORIENTATION_90 = 1;
  52. public static int FRONTEND_ORIENTATION_180 = 2;
  53. public static int FRONTEND_ORIENTATION_270 = 3;
  54. public static int RETRO_RUMBLE_STRONG = 0;
  55. public static int RETRO_RUMBLE_WEAK = 1;
  56. public boolean sustainedPerformanceMode = true;
  57. public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
  58. @Override
  59. protected void onCreate(Bundle savedInstanceState) {
  60. cleanupSymlinks();
  61. updateSymlinks();
  62. PlayCoreManager.getInstance().onCreate(this);
  63. super.onCreate(savedInstanceState);
  64. }
  65. @Override
  66. protected void onDestroy() {
  67. PlayCoreManager.getInstance().onDestroy();
  68. super.onDestroy();
  69. }
  70. public void doVibrate(int id, int effect, int strength, int oneShot)
  71. {
  72. Vibrator vibrator = null;
  73. int repeat = 0;
  74. long[] pattern = {0, 16};
  75. int[] strengths = {0, strength};
  76. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
  77. if (id == -1)
  78. vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
  79. else
  80. {
  81. InputDevice dev = InputDevice.getDevice(id);
  82. if (dev != null)
  83. vibrator = dev.getVibrator();
  84. }
  85. }
  86. if (vibrator == null)
  87. return;
  88. if (strength == 0) {
  89. vibrator.cancel();
  90. return;
  91. }
  92. if (oneShot > 0)
  93. repeat = -1;
  94. else
  95. pattern[1] = 1000;
  96. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  97. if (id >= 0)
  98. Log.i("RetroActivity", "Vibrate id " + id + ": strength " + strength);
  99. vibrator.vibrate(VibrationEffect.createWaveform(pattern, strengths, repeat), new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_GAME).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build());
  100. }else{
  101. vibrator.vibrate(pattern, repeat);
  102. }
  103. }
  104. public void doHapticFeedback(int effect)
  105. {
  106. getWindow().getDecorView().performHapticFeedback(effect,
  107. HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
  108. Log.i("RetroActivity", "Haptic Feedback effect " + effect);
  109. }
  110. // Exiting cleanly from NDK seems to be nearly impossible.
  111. // Have to use exit(0) to avoid weird things happening, even with runOnUiThread() approaches.
  112. // Use a separate JNI function to explicitly trigger the readback.
  113. public void onRetroArchExit()
  114. {
  115. DebugUtil.i(TAG,"onRetroArchExit");
  116. finish();
  117. }
  118. public int getVolumeCount()
  119. {
  120. int ret = 0;
  121. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
  122. StorageManager storageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
  123. List<StorageVolume> storageVolumeList = storageManager.getStorageVolumes();
  124. for (int i = 0; i < storageVolumeList.size(); i++) {
  125. ret++;
  126. }
  127. Log.i("RetroActivity", "volume count: " + ret);
  128. }
  129. return (int)ret;
  130. }
  131. public String getVolumePath(String input)
  132. {
  133. String ret = "";
  134. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
  135. int index = Integer.valueOf(input);
  136. int j = 0;
  137. StorageManager storageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
  138. List<StorageVolume> storageVolumeList = storageManager.getStorageVolumes();
  139. for (int i = 0; i < storageVolumeList.size(); i++) {
  140. if (i == j) {
  141. ret = String.valueOf(storageVolumeList.get(index).getDirectory());
  142. }
  143. }
  144. Log.i("RetroActivity", "volume path: " + ret);
  145. }
  146. return ret;
  147. }
  148. // https://stackoverflow.com/questions/4553650/how-to-check-device-natural-default-orientation-on-android-i-e-get-landscape/4555528#4555528
  149. public int getDeviceDefaultOrientation() {
  150. WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
  151. Configuration config = getResources().getConfiguration();
  152. int rotation = windowManager.getDefaultDisplay().getRotation();
  153. if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) &&
  154. config.orientation == Configuration.ORIENTATION_LANDSCAPE)
  155. || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) &&
  156. config.orientation == Configuration.ORIENTATION_PORTRAIT))
  157. {
  158. return Configuration.ORIENTATION_LANDSCAPE;
  159. }else{
  160. return Configuration.ORIENTATION_PORTRAIT;
  161. }
  162. }
  163. public void setScreenOrientation(int orientation)
  164. {
  165. int naturalOrientation = getDeviceDefaultOrientation();
  166. int newOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
  167. // We assume no device has a natural orientation that is reversed
  168. switch (naturalOrientation) {
  169. case Configuration.ORIENTATION_PORTRAIT:
  170. {
  171. if (orientation == FRONTEND_ORIENTATION_0) {
  172. newOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
  173. }else if (orientation == FRONTEND_ORIENTATION_90) {
  174. newOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
  175. }else if (orientation == FRONTEND_ORIENTATION_180) {
  176. newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
  177. }else if (orientation == FRONTEND_ORIENTATION_270) {
  178. newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
  179. }
  180. break;
  181. }
  182. case Configuration.ORIENTATION_LANDSCAPE:
  183. {
  184. if (orientation == FRONTEND_ORIENTATION_0) {
  185. newOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
  186. }else if (orientation == FRONTEND_ORIENTATION_90) {
  187. newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
  188. }else if (orientation == FRONTEND_ORIENTATION_180) {
  189. newOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
  190. }else if (orientation == FRONTEND_ORIENTATION_270) {
  191. newOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
  192. }
  193. break;
  194. }
  195. }
  196. screenOrientation = newOrientation;
  197. Log.i("RetroActivity", "setting new orientation to " + screenOrientation);
  198. runOnUiThread(new Runnable() {
  199. @Override
  200. public void run() {
  201. setRequestedOrientation(screenOrientation);
  202. }
  203. });
  204. }
  205. public String getUserLanguageString()
  206. {
  207. String lang = Locale.getDefault().getLanguage();
  208. String country = Locale.getDefault().getCountry();
  209. if (lang.length() == 0)
  210. return "en";
  211. if (country.length() == 0)
  212. return lang;
  213. return lang + '_' + country;
  214. }
  215. @TargetApi(24)
  216. public void setSustainedPerformanceMode(boolean on)
  217. {
  218. sustainedPerformanceMode = on;
  219. if (Build.VERSION.SDK_INT >= 24) {
  220. if (isSustainedPerformanceModeSupported()) {
  221. final CountDownLatch latch = new CountDownLatch(1);
  222. runOnUiThread(new Runnable() {
  223. @Override
  224. public void run() {
  225. Log.i("RetroActivity", "setting sustained performance mode to " + sustainedPerformanceMode);
  226. getWindow().setSustainedPerformanceMode(sustainedPerformanceMode);
  227. latch.countDown();
  228. }
  229. });
  230. try {
  231. latch.await();
  232. }catch(InterruptedException e) {
  233. e.printStackTrace();
  234. }
  235. }
  236. }
  237. }
  238. @TargetApi(24)
  239. public boolean isSustainedPerformanceModeSupported()
  240. {
  241. boolean supported = false;
  242. if (Build.VERSION.SDK_INT >= 24)
  243. {
  244. PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
  245. if (powerManager.isSustainedPerformanceModeSupported())
  246. supported = true;
  247. }
  248. Log.i("RetroActivity", "isSustainedPerformanceModeSupported? " + supported);
  249. return supported;
  250. }
  251. public int getBatteryLevel()
  252. {
  253. IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
  254. // 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
  255. Intent batteryStatus = registerReceiver(null, ifilter);
  256. int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
  257. int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
  258. float percent = ((float)level / (float)scale) * 100.0f;
  259. Log.i("RetroActivity", "battery: level = " + level + ", scale = " + scale + ", percent = " + percent);
  260. return (int)percent;
  261. }
  262. public int getPowerstate()
  263. {
  264. IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
  265. // 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
  266. Intent batteryStatus = registerReceiver(null, ifilter);
  267. int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
  268. boolean hasBattery = batteryStatus.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false);
  269. boolean isCharging = (status == BatteryManager.BATTERY_STATUS_CHARGING);
  270. boolean isCharged = (status == BatteryManager.BATTERY_STATUS_FULL);
  271. int powerstate = FRONTEND_POWERSTATE_NONE;
  272. if (isCharged)
  273. powerstate = FRONTEND_POWERSTATE_CHARGED;
  274. else if (isCharging)
  275. powerstate = FRONTEND_POWERSTATE_CHARGING;
  276. else if (!hasBattery)
  277. powerstate = FRONTEND_POWERSTATE_NO_SOURCE;
  278. else
  279. powerstate = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
  280. Log.i("RetroActivity", "power state = " + powerstate);
  281. return powerstate;
  282. }
  283. public boolean isAndroidTV()
  284. {
  285. Configuration config = getResources().getConfiguration();
  286. UiModeManager uiModeManager = (UiModeManager)getSystemService(UI_MODE_SERVICE);
  287. if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
  288. {
  289. Log.i("RetroActivity", "isAndroidTV == true");
  290. return true;
  291. }
  292. else
  293. {
  294. Log.i("RetroActivity", "isAndroidTV == false");
  295. return false;
  296. }
  297. }
  298. @Override
  299. public void onConfigurationChanged(Configuration newConfig) {
  300. int oldOrientation = 0;
  301. boolean hasOldOrientation = false;
  302. super.onConfigurationChanged(newConfig);
  303. Log.i("RetroActivity", "onConfigurationChanged: orientation is now " + newConfig.orientation);
  304. SharedPreferences prefs = UserPreferences.getPreferences(this);
  305. SharedPreferences.Editor edit = prefs.edit();
  306. hasOldOrientation = prefs.contains("ORIENTATION");
  307. if (hasOldOrientation)
  308. oldOrientation = prefs.getInt("ORIENTATION", 0);
  309. edit.putInt("ORIENTATION", newConfig.orientation);
  310. edit.apply();
  311. Log.i("RetroActivity", "hasOldOrientation? " + hasOldOrientation + " newOrientation: " + newConfig.orientation + " oldOrientation: " + oldOrientation);
  312. }
  313. /**
  314. * Checks if this version of RetroArch is a Play Store build.
  315. *
  316. * @return true if this is a Play Store build, false otherwise
  317. */
  318. public boolean isPlayStoreBuild() {
  319. Log.i("RetroActivity", "isPlayStoreBuild: " + BuildConfig.PLAY_STORE_BUILD);
  320. return BuildConfig.PLAY_STORE_BUILD;
  321. }
  322. /**
  323. * Gets the list of available cores that can be downloaded as Dynamic Feature Modules.
  324. *
  325. * @return the list of available cores
  326. */
  327. public String[] getAvailableCores() {
  328. int id = getResources().getIdentifier("module_names_" + Build.CPU_ABI.replace('-', '_'), "array", getPackageName());
  329. String[] returnVal = getResources().getStringArray(id);
  330. Log.i("RetroActivity", "getAvailableCores: " + Arrays.toString(returnVal));
  331. return returnVal;
  332. }
  333. /**
  334. * Gets the list of cores that are currently installed as Dynamic Feature Modules.
  335. *
  336. * @return the list of installed cores
  337. */
  338. public String[] getInstalledCores() {
  339. String[] modules = PlayCoreManager.getInstance().getInstalledModules();
  340. List<String> cores = new ArrayList<>();
  341. List<String> availableCores = Arrays.asList(getAvailableCores());
  342. SharedPreferences prefs = UserPreferences.getPreferences(this);
  343. for(int i = 0; i < modules.length; i++) {
  344. String coreName = unsanitizeCoreName(modules[i]);
  345. if(!prefs.getBoolean("core_deleted_" + coreName, false)
  346. && availableCores.contains(coreName)) {
  347. cores.add(coreName);
  348. }
  349. }
  350. String[] returnVal = cores.toArray(new String[0]);
  351. Log.i("RetroActivity", "getInstalledCores: " + Arrays.toString(returnVal));
  352. return returnVal;
  353. }
  354. /**
  355. * Asks the system to download a core.
  356. *
  357. * @param coreName Name of the core to install
  358. */
  359. public void downloadCore(final String coreName) {
  360. Log.i("RetroActivity", "downloadCore: " + coreName);
  361. SharedPreferences prefs = UserPreferences.getPreferences(this);
  362. prefs.edit().remove("core_deleted_" + coreName).apply();
  363. PlayCoreManager.getInstance().downloadCore(coreName);
  364. }
  365. /**
  366. * Asks the system to delete a core.
  367. *
  368. * Note that the actual module deletion will not happen immediately (the OS will delete
  369. * it whenever it feels like it), but the symlink will still be immediately removed.
  370. *
  371. * @param coreName Name of the core to delete
  372. */
  373. public void deleteCore(String coreName) {
  374. Log.i("RetroActivity", "deleteCore: " + coreName);
  375. String newFilename = getCorePath() + coreName + "_libretro_android.so";
  376. new File(newFilename).delete();
  377. SharedPreferences prefs = UserPreferences.getPreferences(this);
  378. prefs.edit().putBoolean("core_deleted_" + coreName, true).apply();
  379. PlayCoreManager.getInstance().deleteCore(coreName);
  380. }
  381. /////////////// JNI methods ///////////////
  382. public native void clickBack();
  383. /**
  384. * Called when a core install is initiated.
  385. *
  386. * @param coreName Name of the core that the install is initiated for.
  387. * @param successful true if success, false if failure
  388. */
  389. public native void coreInstallInitiated(String coreName, boolean successful);
  390. /**
  391. * Called when the status of a core install has changed.
  392. *
  393. * @param coreNames Names of all cores that are currently being downloaded.
  394. * @param status One of INSTALL_STATUS_DOWNLOADING, INSTALL_STATUS_INSTALLING,
  395. * INSTALL_STATUS_INSTALLED, or INSTALL_STATUS_FAILED
  396. * @param bytesDownloaded Number of bytes downloaded.
  397. * @param totalBytesToDownload Total number of bytes to download.
  398. */
  399. public native void coreInstallStatusChanged(String[] coreNames, int status, long bytesDownloaded, long totalBytesToDownload);
  400. /////////////// Private methods ///////////////
  401. /**
  402. * Sanitizes a core name so that it can be used when dealing with
  403. * Dynamic Feature Modules. Needed because Gradle modules cannot use
  404. * dashes, but we have at least one core name ("mesen-s") that uses them.
  405. *
  406. * @param coreName Name of the core to sanitize.
  407. * @return The sanitized core name.
  408. */
  409. public String sanitizeCoreName(String coreName) {
  410. return "core_" + coreName.replace('-', '_');
  411. }
  412. /**
  413. * Unsanitizes a core name from its module name.
  414. *
  415. * @param coreName Name of the core to unsanitize.
  416. * @return The unsanitized core name.
  417. */
  418. public String unsanitizeCoreName(String coreName) {
  419. if(coreName.equals("core_mesen_s")) {
  420. return "mesen-s";
  421. }
  422. return coreName.substring(5);
  423. }
  424. /**
  425. * Gets the path to the RetroArch cores directory.
  426. *
  427. * @return The path to the RetroArch cores directory
  428. */
  429. private String getCorePath() {
  430. String path = getApplicationInfo().dataDir + "/cores/";
  431. new File(path).mkdirs();
  432. return path;
  433. }
  434. /**
  435. * Cleans up existing symlinks before new ones are created.
  436. */
  437. private void cleanupSymlinks() {
  438. if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
  439. File[] files = new File(getCorePath()).listFiles();
  440. for(int i = 0; i < files.length; i++) {
  441. try {
  442. Os.readlink(files[i].getAbsolutePath());
  443. files[i].delete();
  444. } catch (Exception e) {
  445. // File is not a symlink, so don't delete.
  446. }
  447. }
  448. }
  449. /**
  450. * Triggers a symlink update in the known places that Dynamic Feature Modules
  451. * are installed to.
  452. */
  453. public void updateSymlinks() {
  454. if(!isPlayStoreBuild()) return;
  455. traverseFilesystem(getFilesDir());
  456. traverseFilesystem(new File(getApplicationInfo().nativeLibraryDir));
  457. }
  458. /**
  459. * Traverse the filesystem, looking for native libraries.
  460. * Symlinks any libraries it finds to the main RetroArch "cores" folder,
  461. * updating any existing symlinks with the correct path to the native libraries.
  462. *
  463. * This is necessary because Dynamic Feature Modules are first downloaded
  464. * and installed to a temporary location on disk, before being moved
  465. * to a more permanent location by the system at a later point.
  466. *
  467. * This could probably be done in native code instead, if that's preferred.
  468. *
  469. * @param file The parent directory of the tree to traverse.
  470. */
  471. private void traverseFilesystem(File file) {
  472. if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
  473. File[] list = file.listFiles();
  474. if(list == null) return;
  475. List<String> availableCores = Arrays.asList(getAvailableCores());
  476. // Check each file in a directory to see if it's a native library.
  477. for(int i = 0; i < list.length; i++) {
  478. File child = list[i];
  479. String name = child.getName();
  480. if(name.startsWith("lib") && name.endsWith(".so") && !name.contains("retroarch-activity")) {
  481. // Found a native library!
  482. String core = name.subSequence(3, name.length() - 3).toString();
  483. String filename = child.getAbsolutePath();
  484. SharedPreferences prefs = UserPreferences.getPreferences(this);
  485. if(!prefs.getBoolean("core_deleted_" + core, false)
  486. && availableCores.contains(core)) {
  487. // Generate the destination filename and delete any existing symlinks / cores
  488. String newFilename = getCorePath() + core + "_libretro_android.so";
  489. new File(newFilename).delete();
  490. try {
  491. Os.symlink(filename, newFilename);
  492. } catch (Exception e) {
  493. // Symlink failed to be created. Should never happen.
  494. }
  495. }
  496. } else if(file.isDirectory()) {
  497. // Found another directory, so traverse it
  498. traverseFilesystem(child);
  499. }
  500. }
  501. }
  502. }