GameActivity.java 20 KB

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