GameActivity.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. package org.love2d.android;
  2. import org.libsdl.app.SDLActivity;
  3. import java.util.Arrays;
  4. import java.util.List;
  5. import java.io.BufferedOutputStream;
  6. import java.io.File;
  7. import java.io.FileOutputStream;
  8. import java.io.IOException;
  9. import java.io.InputStream;
  10. import android.Manifest;
  11. import android.app.AlertDialog;
  12. import android.content.Context;
  13. import android.content.DialogInterface;
  14. import android.content.Intent;
  15. import android.media.AudioManager;
  16. import android.net.Uri;
  17. import android.os.Bundle;
  18. import android.os.Environment;
  19. import android.os.Vibrator;
  20. import android.util.Log;
  21. import android.util.DisplayMetrics;
  22. import android.view.*;
  23. import android.content.pm.PackageManager;
  24. import androidx.annotation.Keep;
  25. import androidx.core.app.ActivityCompat;
  26. public class GameActivity extends SDLActivity {
  27. private static DisplayMetrics metrics = new DisplayMetrics();
  28. private static String gamePath = "";
  29. private static Context context;
  30. private static Vibrator vibrator = null;
  31. protected final int[] externalStorageRequestDummy = new int[1];
  32. protected final int[] recordAudioRequestDummy = new int[1];
  33. public static final int EXTERNAL_STORAGE_REQUEST_CODE = 1;
  34. public static final int RECORD_AUDIO_REQUEST_CODE = 2;
  35. private static boolean immersiveActive = false;
  36. private static boolean mustCacheArchive = false;
  37. private boolean storagePermissionUnnecessary = false;
  38. public boolean embed = false;
  39. public int safeAreaTop = 0;
  40. public int safeAreaLeft = 0;
  41. public int safeAreaBottom = 0;
  42. public int safeAreaRight = 0;
  43. @Override
  44. protected String[] getLibraries() {
  45. return new String[]{
  46. "c++_shared",
  47. "mpg123",
  48. "openal",
  49. "hidapi",
  50. "love",
  51. };
  52. }
  53. @Override
  54. protected String getMainSharedObject() {
  55. String[] libs = getLibraries();
  56. String libname = "lib" + libs[libs.length - 1] + ".so";
  57. // Since Lollipop, you can simply pass "libname.so" to dlopen
  58. // and it will resolve correct paths and load correct library.
  59. // This is mandatory for extractNativeLibs=false support in
  60. // Marshmallow.
  61. if (android.os.Build.VERSION.SDK_INT >= 21) {
  62. return libname;
  63. } else {
  64. return getContext().getApplicationInfo().nativeLibraryDir + "/" + libname;
  65. }
  66. }
  67. @Override
  68. protected void onCreate(Bundle savedInstanceState) {
  69. Log.d("GameActivity", "started");
  70. context = this.getApplicationContext();
  71. int res = context.checkCallingOrSelfPermission(Manifest.permission.VIBRATE);
  72. if (res == PackageManager.PERMISSION_GRANTED) {
  73. vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
  74. } else {
  75. Log.d("GameActivity", "Vibration disabled: could not get vibration permission.");
  76. }
  77. // These 2 variables must be reset or it will use the existing value.
  78. gamePath = "";
  79. storagePermissionUnnecessary = false;
  80. embed = context.getResources().getBoolean(R.bool.embed);
  81. handleIntent(this.getIntent());
  82. super.onCreate(savedInstanceState);
  83. getWindowManager().getDefaultDisplay().getMetrics(metrics);
  84. if (android.os.Build.VERSION.SDK_INT >= 28) {
  85. getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
  86. }
  87. }
  88. @Override
  89. protected void onNewIntent(Intent intent) {
  90. Log.d("GameActivity", "onNewIntent() with " + intent);
  91. if (!embed) {
  92. handleIntent(intent);
  93. resetNative();
  94. startNative();
  95. }
  96. }
  97. protected void handleIntent(Intent intent) {
  98. Uri game = intent.getData();
  99. if (!embed && game != null) {
  100. String scheme = game.getScheme();
  101. String path = game.getPath();
  102. // If we have a game via the intent data we we try to figure out how we have to load it. We
  103. // support the following variations:
  104. // * a main.lua file: set gamePath to the directory containing main.lua
  105. // * otherwise: set gamePath to the file
  106. if (scheme.equals("file")) {
  107. Log.d("GameActivity", "Received file:// intent with path: " + path);
  108. // If we were given the path of a main.lua then use its
  109. // directory. Otherwise use full path.
  110. List<String> path_segments = game.getPathSegments();
  111. if (path_segments.get(path_segments.size() - 1).equals("main.lua")) {
  112. gamePath = path.substring(0, path.length() - "main.lua".length());
  113. } else {
  114. gamePath = path;
  115. }
  116. } else if (scheme.equals("content")) {
  117. Log.d("GameActivity", "Received content:// intent with path: " + path);
  118. try {
  119. String filename = "game.love";
  120. String[] pathSegments = path.split("/");
  121. if (pathSegments != null && pathSegments.length > 0) {
  122. filename = pathSegments[pathSegments.length - 1];
  123. }
  124. String destination_file = this.getCacheDir().getPath() + "/" + filename;
  125. InputStream data = getContentResolver().openInputStream(game);
  126. // copyAssetFile automatically closes the InputStream
  127. if (copyAssetFile(data, destination_file)) {
  128. gamePath = destination_file;
  129. storagePermissionUnnecessary = true;
  130. }
  131. } catch (Exception e) {
  132. Log.d("GameActivity", "could not read content uri " + game.toString() + ": " + e.getMessage());
  133. }
  134. } else {
  135. Log.e("GameActivity", "Unsupported scheme: '" + game.getScheme() + "'.");
  136. AlertDialog.Builder alert_dialog = new AlertDialog.Builder(this);
  137. alert_dialog.setMessage("Could not load LÖVE game '" + path
  138. + "' as it uses unsupported scheme '" + game.getScheme()
  139. + "'. Please contact the developer.");
  140. alert_dialog.setTitle("LÖVE for Android Error");
  141. alert_dialog.setPositiveButton("Exit",
  142. new DialogInterface.OnClickListener() {
  143. @Override
  144. public void onClick(DialogInterface dialog, int id) {
  145. finish();
  146. }
  147. });
  148. alert_dialog.setCancelable(false);
  149. alert_dialog.create().show();
  150. }
  151. } else {
  152. // No game specified via the intent data or embed build is used.
  153. // Check whether we have a game.love in our assets.
  154. boolean game_love_in_assets = false;
  155. try {
  156. List<String> assets = Arrays.asList(getAssets().list(""));
  157. game_love_in_assets = assets.contains("game.love");
  158. } catch (Exception e) {
  159. Log.d("GameActivity", "could not list application assets:" + e.getMessage());
  160. }
  161. if (game_love_in_assets) {
  162. // If we have a game.love in our assets folder copy it to the cache folder
  163. // so that we can load it from native LÖVE code
  164. String destination_file = this.getCacheDir().getPath() + "/game.love";
  165. try {
  166. InputStream gameStream = getAssets().open("game.love");
  167. if (mustCacheArchive && copyAssetFile(gameStream, destination_file))
  168. gamePath = destination_file;
  169. else
  170. gamePath = "game.love";
  171. storagePermissionUnnecessary = true;
  172. } catch (IOException e) {
  173. Log.d("GameActivity", "Could not open game.love from assets: " + e.getMessage());
  174. gamePath = "";
  175. storagePermissionUnnecessary = false;
  176. }
  177. } else {
  178. gamePath = "";
  179. storagePermissionUnnecessary = false;
  180. }
  181. }
  182. Log.d("GameActivity", "new gamePath: " + gamePath);
  183. }
  184. protected void checkLovegameFolder() {
  185. // If no game.love was found fall back to the game in <external storage>/lovegame
  186. // if using normal or playstore build
  187. if (!embed) {
  188. Log.d("GameActivity", "fallback to lovegame folder");
  189. if (hasExternalStoragePermission()) {
  190. File ext = Environment.getExternalStorageDirectory();
  191. if ((new File(ext, "/lovegame/main.lua")).exists()) {
  192. gamePath = ext.getPath() + "/lovegame/";
  193. }
  194. } else {
  195. Log.d("GameActivity", "Cannot load game from /sdcard/lovegame: permission not granted");
  196. }
  197. }
  198. }
  199. @Override
  200. protected void onDestroy() {
  201. if (vibrator != null) {
  202. Log.d("GameActivity", "Cancelling vibration");
  203. vibrator.cancel();
  204. }
  205. super.onDestroy();
  206. }
  207. @Override
  208. protected void onPause() {
  209. if (vibrator != null) {
  210. Log.d("GameActivity", "Cancelling vibration");
  211. vibrator.cancel();
  212. }
  213. super.onPause();
  214. }
  215. @Override
  216. public void onResume() {
  217. super.onResume();
  218. if (immersiveActive) {
  219. int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
  220. if (android.os.Build.VERSION.SDK_INT >= 16) {
  221. flags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
  222. View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
  223. View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
  224. View.SYSTEM_UI_FLAG_FULLSCREEN;
  225. }
  226. if (android.os.Build.VERSION.SDK_INT >= 19) {
  227. flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
  228. }
  229. getWindow().getDecorView().setSystemUiVisibility(flags);
  230. }
  231. }
  232. @Keep
  233. public void setImmersiveMode(boolean immersive_mode) {
  234. if (android.os.Build.VERSION.SDK_INT < 11) {
  235. // The API getWindow().getDecorView().setSystemUiVisibility() was
  236. // added in Android 11 (a.k.a. Honeycomb, a.k.a. 3.0.x). If we run
  237. // on this we do nothing.
  238. return;
  239. }
  240. immersiveActive = immersive_mode;
  241. final Object lock = new Object();
  242. final boolean immersive_enabled = immersive_mode;
  243. synchronized (lock) {
  244. runOnUiThread(new Runnable() {
  245. @Override
  246. public void run() {
  247. synchronized (lock) {
  248. if (immersive_enabled) {
  249. int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
  250. if (android.os.Build.VERSION.SDK_INT >= 16) {
  251. flags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
  252. View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
  253. View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
  254. View.SYSTEM_UI_FLAG_FULLSCREEN;
  255. }
  256. if (android.os.Build.VERSION.SDK_INT >= 19) {
  257. flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
  258. }
  259. getWindow().getDecorView().setSystemUiVisibility(flags);
  260. } else if (android.os.Build.VERSION.SDK_INT >= 16) {
  261. getWindow().getDecorView().setSystemUiVisibility(
  262. View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
  263. View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  264. );
  265. }
  266. lock.notify();
  267. }
  268. }
  269. });
  270. }
  271. ;
  272. }
  273. @Keep
  274. public boolean getImmersiveMode() {
  275. return immersiveActive;
  276. }
  277. @Keep
  278. public static String getGamePath() {
  279. GameActivity self = (GameActivity) mSingleton; // use SDL provided one
  280. Log.d("GameActivity", "called getGamePath(), game path = " + gamePath);
  281. if (gamePath.length() > 0) {
  282. if(self.storagePermissionUnnecessary || self.hasExternalStoragePermission()) {
  283. return gamePath;
  284. } else {
  285. Log.d("GameActivity", "cannot open game " + gamePath + ": no external storage permission given!");
  286. }
  287. } else {
  288. self.checkLovegameFolder();
  289. if (gamePath.length() > 0)
  290. return gamePath;
  291. }
  292. return "";
  293. }
  294. public static DisplayMetrics getMetrics() {
  295. return metrics;
  296. }
  297. @Keep
  298. public static void vibrate(double seconds) {
  299. if (vibrator != null) {
  300. vibrator.vibrate((long) (seconds * 1000.));
  301. }
  302. }
  303. @Keep
  304. public static boolean openURL(String url) {
  305. Log.d("GameActivity", "opening url = " + url);
  306. try {
  307. Intent i = new Intent(Intent.ACTION_VIEW);
  308. i.setData(Uri.parse(url));
  309. i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
  310. context.startActivity(i);
  311. return true;
  312. } catch (RuntimeException e) {
  313. Log.d("GameActivity", "love.system.openURL", e);
  314. return false;
  315. }
  316. }
  317. /**
  318. * Copies a given file from the assets folder to the destination.
  319. *
  320. * @return true if successful
  321. */
  322. boolean copyAssetFile(InputStream source_stream, String destinationFileName) {
  323. boolean success = false;
  324. BufferedOutputStream destination_stream = null;
  325. try {
  326. destination_stream = new BufferedOutputStream(new FileOutputStream(destinationFileName, false));
  327. } catch (IOException e) {
  328. Log.d("GameActivity", "Could not open destination file: " + e.getMessage());
  329. }
  330. // perform the copying
  331. int chunk_read = 0;
  332. int bytes_written = 0;
  333. assert (source_stream != null && destination_stream != null);
  334. try {
  335. byte[] buf = new byte[1024];
  336. chunk_read = source_stream.read(buf);
  337. do {
  338. destination_stream.write(buf, 0, chunk_read);
  339. bytes_written += chunk_read;
  340. chunk_read = source_stream.read(buf);
  341. } while (chunk_read != -1);
  342. } catch (IOException e) {
  343. Log.d("GameActivity", "Copying failed:" + e.getMessage());
  344. }
  345. // close streams
  346. try {
  347. if (source_stream != null) source_stream.close();
  348. if (destination_stream != null) destination_stream.close();
  349. success = true;
  350. } catch (IOException e) {
  351. Log.d("GameActivity", "Copying failed: " + e.getMessage());
  352. }
  353. Log.d("GameActivity", "Successfully copied stream to " + destinationFileName + " (" + bytes_written + " bytes written).");
  354. return success;
  355. }
  356. @Keep
  357. public boolean hasBackgroundMusic() {
  358. AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
  359. return audioManager.isMusicActive();
  360. }
  361. @Keep
  362. public void showRecordingAudioPermissionMissingDialog() {
  363. Log.d("GameActivity", "showRecordingAudioPermissionMissingDialog()");
  364. runOnUiThread(new Runnable() {
  365. @Override
  366. public void run() {
  367. AlertDialog dialog = new AlertDialog.Builder(mSingleton)
  368. .setTitle("Audio Recording Permission Missing")
  369. .setMessage("It appears that this game uses mic capabilities. The game may not work correctly without mic permission!")
  370. .setNeutralButton("Continue", new DialogInterface.OnClickListener() {
  371. public void onClick(DialogInterface di, int id) {
  372. synchronized (recordAudioRequestDummy) {
  373. recordAudioRequestDummy.notify();
  374. }
  375. }
  376. })
  377. .create();
  378. dialog.show();
  379. }
  380. });
  381. synchronized (recordAudioRequestDummy) {
  382. try {
  383. recordAudioRequestDummy.wait();
  384. } catch (InterruptedException e) {
  385. Log.d("GameActivity", "mic permission dialog", e);
  386. }
  387. }
  388. }
  389. public void showExternalStoragePermissionMissingDialog() {
  390. AlertDialog dialog = new AlertDialog.Builder(mSingleton)
  391. .setTitle("Storage Permission Missing")
  392. .setMessage("LÖVE for Android will not be able to run non-packaged games without storage permission.")
  393. .setNeutralButton("Continue", null)
  394. .create();
  395. dialog.show();
  396. }
  397. @Override
  398. public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
  399. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  400. if (grantResults.length > 0) {
  401. Log.d("GameActivity", "Received a request permission result");
  402. switch (requestCode) {
  403. case EXTERNAL_STORAGE_REQUEST_CODE: {
  404. if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  405. Log.d("GameActivity", "Permission granted");
  406. } else {
  407. Log.d("GameActivity", "Did not get permission.");
  408. if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
  409. showExternalStoragePermissionMissingDialog();
  410. }
  411. }
  412. Log.d("GameActivity", "Unlocking LÖVE thread");
  413. synchronized (externalStorageRequestDummy) {
  414. externalStorageRequestDummy[0] = grantResults[0];
  415. externalStorageRequestDummy.notify();
  416. }
  417. break;
  418. }
  419. case RECORD_AUDIO_REQUEST_CODE: {
  420. if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  421. Log.d("GameActivity", "Mic ermission granted");
  422. } else {
  423. Log.d("GameActivity", "Did not get mic permission.");
  424. }
  425. Log.d("GameActivity", "Unlocking LÖVE thread");
  426. synchronized (recordAudioRequestDummy) {
  427. recordAudioRequestDummy[0] = grantResults[0];
  428. recordAudioRequestDummy.notify();
  429. }
  430. break;
  431. }
  432. }
  433. }
  434. }
  435. @Keep
  436. public boolean hasExternalStoragePermission() {
  437. if (ActivityCompat.checkSelfPermission(this,
  438. Manifest.permission.READ_EXTERNAL_STORAGE)
  439. == PackageManager.PERMISSION_GRANTED) {
  440. return true;
  441. }
  442. Log.d("GameActivity", "Requesting permission and locking LÖVE thread until we have an answer.");
  443. ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_REQUEST_CODE);
  444. synchronized (externalStorageRequestDummy) {
  445. try {
  446. externalStorageRequestDummy.wait();
  447. } catch (InterruptedException e) {
  448. Log.d("GameActivity", "requesting external storage permission", e);
  449. return false;
  450. }
  451. }
  452. return ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
  453. }
  454. @Keep
  455. public boolean hasRecordAudioPermission() {
  456. return ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
  457. }
  458. @Keep
  459. public void requestRecordAudioPermission() {
  460. if (ActivityCompat.checkSelfPermission(this,
  461. Manifest.permission.RECORD_AUDIO)
  462. == PackageManager.PERMISSION_GRANTED) {
  463. return;
  464. }
  465. Log.d("GameActivity", "Requesting mic permission and locking LÖVE thread until we have an answer.");
  466. ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, RECORD_AUDIO_REQUEST_CODE);
  467. synchronized (recordAudioRequestDummy) {
  468. try {
  469. recordAudioRequestDummy.wait();
  470. } catch (InterruptedException e) {
  471. Log.d("GameActivity", "requesting mic permission", e);
  472. }
  473. }
  474. }
  475. @Keep
  476. public boolean initializeSafeArea() {
  477. if (android.os.Build.VERSION.SDK_INT >= 28) {
  478. DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
  479. if (cutout != null) {
  480. safeAreaTop = cutout.getSafeInsetTop();
  481. safeAreaLeft = cutout.getSafeInsetLeft();
  482. safeAreaBottom = cutout.getSafeInsetBottom();
  483. safeAreaRight = cutout.getSafeInsetRight();
  484. return true;
  485. }
  486. }
  487. return false;
  488. }
  489. }