GameActivity.java 12 KB

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