GameActivity.java 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /*
  2. * Copyright (c) 2006-2022 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 androidx.annotation.Keep;
  22. import org.libsdl.app.SDLActivity;
  23. import android.Manifest;
  24. import android.content.Context;
  25. import android.content.Intent;
  26. import android.content.pm.ApplicationInfo;
  27. import android.content.pm.PackageManager;
  28. import android.content.res.AssetManager;
  29. import android.graphics.Rect;
  30. import android.media.AudioManager;
  31. import android.net.Uri;
  32. import android.os.Bundle;
  33. import android.os.ParcelFileDescriptor;
  34. import android.os.VibrationEffect;
  35. import android.os.Vibrator;
  36. import android.util.DisplayMetrics;
  37. import android.util.Log;
  38. import android.view.DisplayCutout;
  39. import android.view.WindowManager;
  40. import java.io.File;
  41. import java.io.FileNotFoundException;
  42. import java.io.IOException;
  43. import java.io.InputStream;
  44. import java.util.ArrayList;
  45. import java.util.HashMap;
  46. import java.util.Map;
  47. public class GameActivity extends SDLActivity {
  48. private static final String TAG = "GameActivity";
  49. protected Vibrator vibrator;
  50. protected boolean shortEdgesMode;
  51. private int delayedFd = -1;
  52. private String[] args = new String[0];
  53. private boolean isFused;
  54. private static native void nativeSetDefaultStreamValues(int sampleRate, int framesPerBurst);
  55. @Override
  56. protected String getMainSharedObject() {
  57. String[] libs = getLibraries();
  58. // Since Lollipop, you can simply pass "libname.so" to dlopen
  59. // and it will resolve correct paths and load correct library.
  60. // This is mandatory for extractNativeLibs=false support in
  61. // Marshmallow.
  62. return "lib" + libs[libs.length - 1] + ".so";
  63. }
  64. @Override
  65. protected String[] getLibraries() {
  66. return new String[] {
  67. "c++_shared",
  68. "SDL2",
  69. "openal",
  70. "luajit",
  71. "love",
  72. };
  73. }
  74. @Override
  75. protected String[] getArguments() {
  76. return args;
  77. }
  78. @Override
  79. protected void onCreate(Bundle savedInstanceState) {
  80. Log.d(TAG, "started");
  81. isFused = hasEmbeddedGame();
  82. if (checkCallingOrSelfPermission(Manifest.permission.VIBRATE) == PackageManager.PERMISSION_GRANTED) {
  83. vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
  84. }
  85. Intent intent = getIntent();
  86. super.onCreate(savedInstanceState);
  87. handleIntent(intent);
  88. // Set low-latency audio values
  89. nativeSetDefaultStreamValues(getAudioFreq(), getAudioSMP());
  90. if (android.os.Build.VERSION.SDK_INT >= 28) {
  91. WindowManager.LayoutParams attr = getWindow().getAttributes();
  92. attr.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
  93. shortEdgesMode = false;
  94. }
  95. if (delayedFd != -1) {
  96. // This delayed fd is only sent if an embedded game is present.
  97. sendFileDescriptorAsDroppedFile(delayedFd);
  98. delayedFd = -1;
  99. }
  100. }
  101. @Override
  102. protected void onNewIntent(Intent intent) {
  103. super.onNewIntent(intent);
  104. handleIntent(intent);
  105. }
  106. @Override
  107. protected void onDestroy() {
  108. if (vibrator != null) {
  109. Log.d(TAG, "Cancelling vibration");
  110. vibrator.cancel();
  111. }
  112. super.onDestroy();
  113. }
  114. @Override
  115. protected void onPause() {
  116. if (vibrator != null) {
  117. Log.d(TAG, "Cancelling vibration");
  118. vibrator.cancel();
  119. }
  120. super.onPause();
  121. }
  122. @Keep
  123. public static DisplayMetrics getDisplayMetrics() {
  124. return getDisplayDPI();
  125. }
  126. @Keep
  127. public boolean hasEmbeddedGame() {
  128. AssetManager am = getAssets();
  129. InputStream inputStream;
  130. try {
  131. // Prioritize main.lua in assets folder
  132. inputStream = am.open("main.lua");
  133. } catch (IOException e) {
  134. // Not found, try game.love in assets folder
  135. try {
  136. inputStream = am.open("game.love");
  137. } catch (IOException e2) {
  138. // Not found
  139. return false;
  140. }
  141. }
  142. if (inputStream != null) {
  143. try {
  144. inputStream.close();
  145. } catch (IOException e) {
  146. // Ignored
  147. }
  148. }
  149. return inputStream != null;
  150. }
  151. @Keep
  152. public void vibrate(double seconds) {
  153. if (vibrator != null) {
  154. long duration = (long) (seconds * 1000.);
  155. if (android.os.Build.VERSION.SDK_INT >= 26) {
  156. VibrationEffect ve = VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE);
  157. vibrator.vibrate(ve);
  158. } else {
  159. vibrator.vibrate(duration);
  160. }
  161. }
  162. }
  163. @Keep
  164. public static boolean openURLFromLOVE(String url) {
  165. Log.d(TAG, "opening url = " + url);
  166. return openURL(url) == 0;
  167. }
  168. @Keep
  169. public boolean hasBackgroundMusic() {
  170. AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
  171. return audioManager.isMusicActive();
  172. }
  173. @Keep
  174. public String[] buildFileTree() {
  175. // Map key is path, value is directory flag
  176. HashMap<String, Boolean> map = buildFileTree(getAssets(), "", new HashMap<>());
  177. ArrayList<String> result = new ArrayList<>();
  178. for (Map.Entry<String, Boolean> data: map.entrySet()) {
  179. result.add((data.getValue() ? "d" : "f") + data.getKey());
  180. }
  181. String[] r = new String[result.size()];
  182. result.toArray(r);
  183. return r;
  184. }
  185. @Keep
  186. public float getDPIScale() {
  187. DisplayMetrics metrics = getResources().getDisplayMetrics();
  188. return metrics.density;
  189. }
  190. @Keep
  191. public Rect getSafeArea() {
  192. Rect rect = null;
  193. if (android.os.Build.VERSION.SDK_INT >= 28) {
  194. DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
  195. if (cutout != null) {
  196. rect = new Rect();
  197. rect.set(
  198. cutout.getSafeInsetLeft(),
  199. cutout.getSafeInsetTop(),
  200. cutout.getSafeInsetRight(),
  201. cutout.getSafeInsetBottom()
  202. );
  203. }
  204. }
  205. return rect;
  206. }
  207. @Keep
  208. public String getCRequirePath() {
  209. ApplicationInfo applicationInfo = getApplicationInfo();
  210. if (isNativeLibsExtracted()) {
  211. return applicationInfo.nativeLibraryDir + "/?.so";
  212. } else {
  213. // The native libs are inside the APK and can be loaded directly.
  214. // FIXME: What about split APKs?
  215. String abi = android.os.Build.SUPPORTED_ABIS[0];
  216. return applicationInfo.sourceDir + "!/lib/" + abi + "/?.so";
  217. }
  218. }
  219. @Keep
  220. public void setImmersiveMode(boolean enable) {
  221. if (android.os.Build.VERSION.SDK_INT >= 28) {
  222. WindowManager.LayoutParams attr = getWindow().getAttributes();
  223. if (enable) {
  224. attr.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
  225. } else {
  226. attr.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
  227. }
  228. }
  229. shortEdgesMode = enable;
  230. }
  231. @Keep
  232. public boolean getImmersiveMode() {
  233. return shortEdgesMode;
  234. }
  235. public int getAudioSMP() {
  236. int smp = 256;
  237. AudioManager a = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
  238. if (a != null) {
  239. int b = Integer.parseInt(a.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER));
  240. smp = b > 0 ? b : smp;
  241. }
  242. return smp;
  243. }
  244. public int getAudioFreq() {
  245. int freq = 44100;
  246. AudioManager a = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
  247. if (a != null) {
  248. int b = Integer.parseInt(a.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE));
  249. freq = b > 0 ? b : freq;
  250. }
  251. return freq;
  252. }
  253. public boolean isNativeLibsExtracted() {
  254. if (android.os.Build.VERSION.SDK_INT >= 23) {
  255. ApplicationInfo appInfo = getApplicationInfo();
  256. return (appInfo.flags & ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) != 0;
  257. }
  258. return false;
  259. }
  260. public void sendFileDescriptorAsDroppedFile(int fd) {
  261. if (fd != -1) {
  262. SDLActivity.onNativeDropFile("love2d://fd/" + fd);
  263. }
  264. }
  265. private void handleIntent(Intent intent) {
  266. Uri game = intent.getData();
  267. if (game == null) {
  268. return;
  269. }
  270. if (mSingleton == null) {
  271. // Game is not running, consider setting the currentGameInfo here
  272. if (isFused) {
  273. // Send it as dropped file later
  274. delayedFd = convertToFileDescriptor(game);
  275. } else {
  276. // Process to GameInfo
  277. processOpenGame(intent, game);
  278. }
  279. } else {
  280. // Game is already running. Send it as dropped file.
  281. int fd = convertToFileDescriptor(game);
  282. sendFileDescriptorAsDroppedFile(fd);
  283. }
  284. }
  285. private HashMap<String, Boolean> buildFileTree(AssetManager assetManager, String dir, HashMap<String, Boolean> map) {
  286. String strippedDir = dir.endsWith("/") ? dir.substring(0, dir.length() - 1) : dir;
  287. // Try open dir
  288. try {
  289. InputStream test = assetManager.open(strippedDir);
  290. // It's a file
  291. test.close();
  292. map.put(strippedDir, false);
  293. } catch (FileNotFoundException e) {
  294. // It's a directory
  295. String[] list = null;
  296. // List files
  297. try {
  298. list = assetManager.list(strippedDir);
  299. } catch (IOException e2) {
  300. Log.e(TAG, strippedDir, e2);
  301. }
  302. // Mark as file
  303. map.put(dir, true);
  304. // This Object comparison is intentional.
  305. if (strippedDir != dir) {
  306. map.put(strippedDir, true);
  307. }
  308. if (list != null) {
  309. for (String path: list) {
  310. buildFileTree(assetManager, dir + path + "/", map);
  311. }
  312. }
  313. } catch (IOException e) {
  314. Log.e(TAG, dir, e);
  315. }
  316. return map;
  317. }
  318. private int convertToFileDescriptor(Uri uri) {
  319. try {
  320. ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
  321. return pfd.dup().detachFd();
  322. } catch (IOException e) {
  323. Log.d(TAG, "Failed attempt to convert " + uri.toString() + " to file descriptor", e);
  324. }
  325. return -1;
  326. }
  327. private void processOpenGame(Intent intent, Uri game) {
  328. String scheme = game.getScheme();
  329. String path = game.getPath();
  330. if (scheme.equals("content")) {
  331. // The intent may have more information about the filename
  332. args = new String[] {"/love2d://fd/" + convertToFileDescriptor(game)};
  333. } else if (scheme.equals("file")) {
  334. args = new String[] {path};
  335. }
  336. }
  337. }