diff --git a/app/src/main/java/org/love2d/android/GameActivity.java b/app/src/main/java/org/love2d/android/GameActivity.java index b6ba14fa..4c496915 100644 --- a/app/src/main/java/org/love2d/android/GameActivity.java +++ b/app/src/main/java/org/love2d/android/GameActivity.java @@ -1,14 +1,403 @@ +/* + * Copyright (c) 2006-2022 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + package org.love2d.android; -import androidx.appcompat.app.AppCompatActivity; +import androidx.annotation.Keep; + import org.libsdl.app.SDLActivity; +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.graphics.Rect; +import android.media.AudioManager; +import android.net.Uri; import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.DisplayCutout; +import android.view.WindowManager; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; public class GameActivity extends SDLActivity { + private static final String TAG = "GameActivity"; + + protected Vibrator vibrator; + protected boolean shortEdgesMode; + private GameInfo currentGameInfo; + private int delayedFd = -1; + + private static native void nativeSetDefaultStreamValues(int sampleRate, int framesPerBurst); + + @Override + protected String getMainSharedObject() { + String[] libs = getLibraries(); + // Since Lollipop, you can simply pass "libname.so" to dlopen + // and it will resolve correct paths and load correct library. + // This is mandatory for extractNativeLibs=false support in + // Marshmallow. + return "lib" + libs[libs.length - 1] + ".so"; + } + + @Override + protected String[] getLibraries() { + return new String[] { + "c++_shared", + "SDL2", + "openal", + "luajit", + "love", + }; + } @Override protected void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "started"); + + if (checkCallingOrSelfPermission(Manifest.permission.VIBRATE) == PackageManager.PERMISSION_GRANTED) { + vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + } + + currentGameInfo = new GameInfo(); + + handleIntent(getIntent()); super.onCreate(savedInstanceState); + + // Set low-latency audio values + nativeSetDefaultStreamValues(getAudioFreq(), getAudioSMP()); + + if (android.os.Build.VERSION.SDK_INT >= 28) { + WindowManager.LayoutParams attr = getWindow().getAttributes(); + attr.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + shortEdgesMode = false; + } + + if (delayedFd != -1) { + // This delayed fd is only sent if an embedded game is present. + SDLActivity.onNativeDropFile("love2d://fd/" + delayedFd); + delayedFd = -1; + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + @Override + protected void onDestroy() { + if (vibrator != null) { + Log.d(TAG, "Cancelling vibration"); + vibrator.cancel(); + } + super.onDestroy(); + } + + @Override + protected void onPause() { + if (vibrator != null) { + Log.d(TAG, "Cancelling vibration"); + vibrator.cancel(); + } + super.onPause(); + } + + @Keep + public boolean hasEmbeddedGame() { + AssetManager am = getAssets(); + InputStream inputStream; + + try { + // Prioritize main.lua in assets folder + inputStream = am.open("main.lua"); + } catch (IOException e) { + // Not found, try game.love in assets folder + try { + inputStream = am.open("game.love"); + } catch (IOException e2) { + // Not found + return false; + } + } + + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + // Ignored + } + } + + return inputStream != null; + } + + @Keep + public GameInfo getGameInfo() { + return currentGameInfo; + } + + @Keep + public void vibrate(double seconds) { + if (vibrator != null) { + long duration = (long) (seconds * 1000.); + if (android.os.Build.VERSION.SDK_INT >= 26) { + VibrationEffect ve = VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE); + vibrator.vibrate(ve); + } else { + vibrator.vibrate(duration); + } + } + } + + @Keep + public boolean openURLFromLOVE(String url) { + Log.d(TAG, "opening url = " + url); + return openURL(url) == 0; + } + + @Keep + public boolean hasBackgroundMusic() { + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + return audioManager.isMusicActive(); + } + + @Keep + public String[] buildFileTree() { + // Map key is path, value is directory flag + HashMap map = buildFileTree(getAssets(), "", new HashMap<>()); + ArrayList result = new ArrayList<>(); + + for (Map.Entry data: map.entrySet()) { + result.add((data.getValue() ? "d" : "f") + data.getKey()); + } + + String[] r = new String[result.size()]; + result.toArray(r); + return r; + } + + @Keep + public float getDPIScale() { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + return metrics.density; + } + + @Keep + public Rect getSafeArea() { + Rect rect = null; + + if (android.os.Build.VERSION.SDK_INT >= 28) { + DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout(); + + if (cutout != null) { + rect = new Rect(); + rect.set( + cutout.getSafeInsetLeft(), + cutout.getSafeInsetTop(), + cutout.getSafeInsetRight(), + cutout.getSafeInsetBottom() + ); + } + } + + return rect; + } + + @Keep + public String getCRequirePath() { + ApplicationInfo applicationInfo = getApplicationInfo(); + + if (isNativeLibsExtracted()) { + return applicationInfo.nativeLibraryDir + "/?.so"; + } else { + // The native libs are inside the APK and can be loaded directly. + // FIXME: What about split APKs? + String abi = android.os.Build.SUPPORTED_ABIS[0]; + return applicationInfo.sourceDir + "!/lib/" + abi + "/?.so"; + } + } + + @Keep + public void setImmersiveMode(boolean enable) { + if (android.os.Build.VERSION.SDK_INT >= 28) { + WindowManager.LayoutParams attr = getWindow().getAttributes(); + + if (enable) { + attr.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } else { + attr.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + } + } + + shortEdgesMode = enable; + } + + @Keep + public boolean getImmersiveMode() { + return shortEdgesMode; + } + + public int getAudioSMP() { + int smp = 256; + AudioManager a = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + if (a != null) { + int b = Integer.parseInt(a.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)); + smp = b > 0 ? b : smp; + } + + return smp; + } + + public int getAudioFreq() { + int freq = 44100; + AudioManager a = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + if (a != null) { + int b = Integer.parseInt(a.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)); + freq = b > 0 ? b : freq; + } + + return freq; + } + + public boolean isNativeLibsExtracted() { + if (android.os.Build.VERSION.SDK_INT >= 23) { + ApplicationInfo appInfo = getApplicationInfo(); + return (appInfo.flags & ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) != 0; + } + + return false; + } + + private void handleIntent(Intent intent) { + Uri game = intent.getData(); + if (game == null) { + return; + } + + if (mSingleton == null) { + // Game is not running, consider setting the currentGameInfo here + + if (hasEmbeddedGame()) { + // Send it as dropped file later + delayedFd = convertToFileDescriptor(game); + } else { + // Process to GameInfo + processOpenGame(intent, game); + } + } else { + // Game is already running. Send it as dropped file. + int fd = convertToFileDescriptor(game); + if (fd != -1) { + SDLActivity.onNativeDropFile("love2d://fd/" + fd); + } + } + } + + private HashMap buildFileTree(AssetManager assetManager, String dir, HashMap map) { + String strippedDir = dir.endsWith("/") ? dir.substring(0, dir.length() - 1) : dir; + + // Try open dir + try { + InputStream test = assetManager.open(strippedDir); + // It's a file + test.close(); + map.put(strippedDir, false); + } catch (FileNotFoundException e) { + // It's a directory + String[] list = null; + + // List files + try { + list = assetManager.list(strippedDir); + } catch (IOException e2) { + Log.e(TAG, strippedDir, e2); + } + + // Mark as file + map.put(dir, true); + + // This Object comparison is intentional. + if (strippedDir != dir) { + map.put(strippedDir, true); + } + + if (list != null) { + for (String path: list) { + buildFileTree(assetManager, dir + path + "/", map); + } + } + } catch (IOException e) { + Log.e(TAG, dir, e); + } + + return map; + } + + private int convertToFileDescriptor(Uri uri) { + try { + ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r"); + return pfd.getFd(); + } catch (FileNotFoundException e) { + Log.d(TAG, "Failed attempt to convert " + uri.toString() + " to file descriptor", e); + } + + return -1; + } + + private void processOpenGame(Intent intent, Uri game) { + String scheme = game.getScheme(); + String path = game.getPath(); + + if (scheme.equals("content")) { + // The intent may have more information about the filename + currentGameInfo.identity = intent.getStringExtra("name"); + + if (currentGameInfo.identity == null) { + // Use "lovegame" as fallback + // TODO: Use the content URI basename + Log.w(TAG, "Using \"lovegame\" as fallback for game identity (Uri " + game + ")"); + currentGameInfo.identity = "lovegame"; + } + + currentGameInfo.fd = convertToFileDescriptor(game); + } else if (scheme.equals("file")) { + File f = new File(path); + currentGameInfo.path = path; + currentGameInfo.identity = f.getName(); + } } } diff --git a/app/src/main/java/org/love2d/android/GameInfo.java b/app/src/main/java/org/love2d/android/GameInfo.java new file mode 100644 index 00000000..5aab7f44 --- /dev/null +++ b/app/src/main/java/org/love2d/android/GameInfo.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2006-2022 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +package org.love2d.android; + +public class GameInfo { + int fd; + String identity; + String path; + // TODO: Custom Filesystem class for users who develop using Android. + // but evaluate if it's worth it + + GameInfo() { + fd = -1; + } + + public int getFd() { + return fd; + } + + public String getPath() { + return path; + } + + public String getIdentity() { + return identity; + } +} diff --git a/app/src/normal/AndroidManifest.xml b/app/src/normal/AndroidManifest.xml index 81a2d7b3..2ba28217 100644 --- a/app/src/normal/AndroidManifest.xml +++ b/app/src/normal/AndroidManifest.xml @@ -8,7 +8,6 @@