| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645 |
- /**
- * Copyright (c) 2006-2023 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 org.libsdl.app.SDLActivity;
- import java.io.BufferedOutputStream;
- import java.io.File;
- import java.io.FileNotFoundException;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import android.Manifest;
- import android.app.AlertDialog;
- import android.content.Context;
- import android.content.DialogInterface;
- import android.content.Intent;
- import android.content.pm.ApplicationInfo;
- import android.content.res.AssetManager;
- import android.media.AudioManager;
- import android.net.Uri;
- import android.os.Bundle;
- import android.os.Environment;
- import android.os.Vibrator;
- import android.util.Log;
- import android.util.DisplayMetrics;
- import android.view.*;
- import android.content.pm.PackageManager;
- import androidx.annotation.Keep;
- import androidx.core.app.ActivityCompat;
- public class GameActivity extends SDLActivity {
- private static DisplayMetrics metrics = null;
- private static String gamePath = "";
- private static Vibrator vibrator = null;
- protected final int[] externalStorageRequestDummy = new int[1];
- protected final int[] recordAudioRequestDummy = new int[1];
- public static final int EXTERNAL_STORAGE_REQUEST_CODE = 2;
- public static final int RECORD_AUDIO_REQUEST_CODE = 3;
- private static boolean immersiveActive = false;
- private static boolean needToCopyGameInArchive = false;
- private boolean storagePermissionUnnecessary = false;
- private boolean shortEdgesMode = false;
- public boolean embed = false;
- public int safeAreaTop = 0;
- public int safeAreaLeft = 0;
- public int safeAreaBottom = 0;
- public int safeAreaRight = 0;
- private static native void nativeSetDefaultStreamValues(int sampleRate, int framesPerBurst);
- @Override
- protected String[] getLibraries() {
- return new String[] {
- "c++_shared",
- "mpg123",
- "openal",
- "love",
- };
- }
- @Override
- protected String getMainSharedObject() {
- String[] libs = getLibraries();
- String libname = "lib" + libs[libs.length - 1] + ".so";
- // 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.
- if (android.os.Build.VERSION.SDK_INT >= 21) {
- return libname;
- } else {
- return getContext().getApplicationInfo().nativeLibraryDir + "/" + libname;
- }
- }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- Log.d("GameActivity", "started");
- int res = checkCallingOrSelfPermission(Manifest.permission.VIBRATE);
- if (res == PackageManager.PERMISSION_GRANTED) {
- vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
- } else {
- Log.d("GameActivity", "Vibration disabled: could not get vibration permission.");
- }
- // These 2 variables must be reset or it will use the existing value.
- gamePath = "";
- storagePermissionUnnecessary = false;
- embed = getResources().getBoolean(R.bool.embed);
- needToCopyGameInArchive = embed;
- if (!embed) {
- Intent intent = getIntent();
- handleIntent(intent);
- intent.setData(null);
- }
- super.onCreate(savedInstanceState);
- metrics = getResources().getDisplayMetrics();
- // Set low-latency audio values
- nativeSetDefaultStreamValues(getAudioFreq(), getAudioSMP());
- if (android.os.Build.VERSION.SDK_INT >= 28) {
- getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
- shortEdgesMode = false;
- }
- }
- @Override
- protected void onNewIntent(Intent intent) {
- Log.d("GameActivity", "onNewIntent() with " + intent);
- if (!embed) {
- handleIntent(intent);
- resetNative();
- startNative();
- }
- }
- protected void handleIntent(Intent intent) {
- Uri game = intent.getData();
- if (!embed && game != null) {
- String scheme = game.getScheme();
- String path = game.getPath();
- // If we have a game via the intent data we we try to figure out how we have to load it. We
- // support the following variations:
- // * a main.lua file: set gamePath to the directory containing main.lua
- // * otherwise: set gamePath to the file
- if (scheme.equals("file")) {
- Log.d("GameActivity", "Received file:// intent with path: " + path);
- // If we were given the path of a main.lua then use its
- // directory. Otherwise use full path.
- List<String> path_segments = game.getPathSegments();
- if (path_segments.get(path_segments.size() - 1).equals("main.lua")) {
- gamePath = path.substring(0, path.length() - "main.lua".length());
- } else {
- gamePath = path;
- }
- } else if (scheme.equals("content")) {
- Log.d("GameActivity", "Received content:// intent with path: " + path);
- try {
- String filename = "game.love";
- String[] pathSegments = path.split("/");
- if (pathSegments.length > 0) {
- filename = pathSegments[pathSegments.length - 1];
- }
- // Sanitize filename to prevent PhysFS complaining later.
- filename = filename.replaceAll("[^a-zA-Z0-9_\\\\-\\\\.]", "_");
- String destination_file = this.getCacheDir().getPath() + "/" + filename;
- InputStream data = getContentResolver().openInputStream(game);
- // copyAssetFile automatically closes the InputStream
- if (copyAssetFile(data, destination_file)) {
- gamePath = destination_file;
- storagePermissionUnnecessary = true;
- }
- } catch (Exception e) {
- Log.d("GameActivity", "could not read content uri " + game.toString() + ": " + e.getMessage());
- }
- } else {
- Log.e("GameActivity", "Unsupported scheme: '" + game.getScheme() + "'.");
- AlertDialog.Builder alert_dialog = new AlertDialog.Builder(this);
- alert_dialog.setMessage("Could not load LÖVE game '" + path
- + "' as it uses unsupported scheme '" + game.getScheme()
- + "'. Please contact the developer.");
- alert_dialog.setTitle("LÖVE for Android Error");
- alert_dialog.setPositiveButton("Exit",
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int id) {
- finish();
- }
- });
- alert_dialog.setCancelable(false);
- alert_dialog.create().show();
- }
- }
- Log.d("GameActivity", "new gamePath: " + gamePath);
- }
- private void copyGameInsideArchive() {
- try {
- // If we have a game.love in our assets folder copy it to the cache folder
- // so that we can load it from native LÖVE code
- AssetManager assetManager = getAssets();
- InputStream gameStream = assetManager.open("game.love");
- String destinationFile = this.getCacheDir().getPath() + "/game.love";
- if (copyAssetFile(gameStream, destinationFile))
- gamePath = destinationFile;
- else
- gamePath = "game.love";
- storagePermissionUnnecessary = true;
- } catch (IOException e) {
- // There's no game.love in our assets
- Log.d("GameActivity", "Could not open game.love from assets: " + e.getMessage());
- }
- }
- protected void checkLovegameFolder() {
- // If no game.love was found and embed flavor is not used, fall back to the game in
- // <external storage>/Android/data/<package name>/games/lovegame
- if (!embed) {
- Log.d("GameActivity", "fallback to lovegame folder");
- File ext = getExternalFilesDir("games");
- if ((new File(ext, "/lovegame/main.lua")).exists()) {
- gamePath = ext.getPath() + "/lovegame/";
- storagePermissionUnnecessary = true;
- } else if (android.os.Build.VERSION.SDK_INT <= 28) {
- // Try to fallback to /sdcard/lovegame in Android 9 and earlier too.
- if (hasExternalStoragePermission()) {
- ext = Environment.getExternalStorageDirectory();
- if ((new File(ext, "/lovegame/main.lua")).exists()) {
- gamePath = ext.getPath() + "/lovegame/";
- storagePermissionUnnecessary = false;
- }
- } else {
- Log.d("GameActivity", "Cannot load game from /sdcard/lovegame: permission not granted");
- }
- }
- Log.d("GameActivity", "lovegame directory: " + gamePath);
- }
- }
- @Override
- protected void onDestroy() {
- if (vibrator != null) {
- Log.d("GameActivity", "Cancelling vibration");
- vibrator.cancel();
- }
- super.onDestroy();
- }
- @Override
- protected void onPause() {
- if (vibrator != null) {
- Log.d("GameActivity", "Cancelling vibration");
- vibrator.cancel();
- }
- super.onPause();
- }
- @Override
- public void onResume() {
- super.onResume();
- }
- @Keep
- public void setImmersiveMode(boolean immersive_mode) {
- if (android.os.Build.VERSION.SDK_INT >= 28) {
- getWindow().getAttributes().layoutInDisplayCutoutMode = immersive_mode ?
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES :
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
- shortEdgesMode = immersive_mode;
- }
- immersiveActive = immersive_mode;
- }
- @Keep
- public boolean getImmersiveMode() {
- return immersiveActive;
- }
- @Keep
- public static String getGamePath() {
- GameActivity self = (GameActivity) mSingleton; // use SDL provided one
- Log.d("GameActivity", "called getGamePath(), game path = " + gamePath);
- if (gamePath.length() > 0) {
- if (self.storagePermissionUnnecessary || self.hasExternalStoragePermission()) {
- return gamePath;
- } else {
- Log.d("GameActivity", "cannot open game " + gamePath + ": no external storage permission given!");
- }
- } else if (needToCopyGameInArchive) {
- self.copyGameInsideArchive();
- } else {
- self.checkLovegameFolder();
- }
- return gamePath;
- }
- public static DisplayMetrics getMetrics() {
- return metrics;
- }
- @Keep
- public static void vibrate(double seconds) {
- if (vibrator != null) {
- vibrator.vibrate((long) (seconds * 1000.));
- }
- }
- @Keep
- public static boolean openURLFromLOVE(String url) {
- Log.d("GameActivity", "opening url = " + url);
- return openURL(url) == 0;
- }
- /**
- * Copies a given file from the assets folder to the destination.
- *
- * @return true if successful
- */
- boolean copyAssetFile(InputStream source, String destinationFileName) {
- boolean success = false;
- BufferedOutputStream destination = null;
- try {
- destination = new BufferedOutputStream(new FileOutputStream(destinationFileName, false));
- } catch (IOException e) {
- Log.d("GameActivity", "Could not open destination file: " + e.getMessage());
- }
- // perform the copying
- int chunk_read;
- int bytes_written = 0;
- assert (source != null && destination != null);
- try {
- byte[] buf = new byte[1024];
- chunk_read = source.read(buf);
- do {
- destination.write(buf, 0, chunk_read);
- bytes_written += chunk_read;
- chunk_read = source.read(buf);
- } while (chunk_read != -1);
- } catch (IOException e) {
- Log.d("GameActivity", "Copying failed:" + e.getMessage());
- }
- // close streams
- try {
- source.close();
- destination.close();
- success = true;
- } catch (IOException e) {
- Log.d("GameActivity", "Copying failed: " + e.getMessage());
- }
- Log.d("GameActivity", "Successfully copied stream to " + destinationFileName + " (" + bytes_written + " bytes written).");
- return success;
- }
- @Keep
- public boolean hasBackgroundMusic() {
- AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- return audioManager.isMusicActive();
- }
- @Keep
- public void showRecordingAudioPermissionMissingDialog() {
- Log.d("GameActivity", "showRecordingAudioPermissionMissingDialog()");
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- AlertDialog dialog = new AlertDialog.Builder(mSingleton)
- .setTitle("Audio Recording Permission Missing")
- .setMessage("It appears that this game uses mic capabilities. The game may not work correctly without mic permission!")
- .setNeutralButton("Continue", new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface di, int id) {
- synchronized (recordAudioRequestDummy) {
- recordAudioRequestDummy.notify();
- }
- }
- })
- .create();
- dialog.show();
- }
- });
- synchronized (recordAudioRequestDummy) {
- try {
- recordAudioRequestDummy.wait();
- } catch (InterruptedException e) {
- Log.d("GameActivity", "mic permission dialog", e);
- }
- }
- }
- public void showExternalStoragePermissionMissingDialog() {
- AlertDialog dialog = new AlertDialog.Builder(mSingleton)
- .setTitle("Storage Permission Missing")
- .setMessage("LÖVE for Android will not be able to run non-packaged games without storage permission.")
- .setNeutralButton("Continue", null)
- .create();
- dialog.show();
- }
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- if (grantResults.length > 0) {
- Log.d("GameActivity", "Received a request permission result");
- switch (requestCode) {
- case EXTERNAL_STORAGE_REQUEST_CODE: {
- if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- Log.d("GameActivity", "Permission granted");
- } else {
- Log.d("GameActivity", "Did not get permission.");
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
- showExternalStoragePermissionMissingDialog();
- }
- }
- Log.d("GameActivity", "Unlocking LÖVE thread");
- synchronized (externalStorageRequestDummy) {
- externalStorageRequestDummy[0] = grantResults[0];
- externalStorageRequestDummy.notify();
- }
- break;
- }
- case RECORD_AUDIO_REQUEST_CODE: {
- if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- Log.d("GameActivity", "Mic permission granted");
- } else {
- Log.d("GameActivity", "Did not get mic permission.");
- }
- Log.d("GameActivity", "Unlocking LÖVE thread");
- synchronized (recordAudioRequestDummy) {
- recordAudioRequestDummy[0] = grantResults[0];
- recordAudioRequestDummy.notify();
- }
- break;
- }
- default:
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- }
- }
- }
- @Keep
- public boolean hasExternalStoragePermission() {
- if (ActivityCompat.checkSelfPermission(this,
- Manifest.permission.READ_EXTERNAL_STORAGE)
- == PackageManager.PERMISSION_GRANTED) {
- return true;
- }
- Log.d("GameActivity", "Requesting permission and locking LÖVE thread until we have an answer.");
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_REQUEST_CODE);
- synchronized (externalStorageRequestDummy) {
- try {
- externalStorageRequestDummy.wait();
- } catch (InterruptedException e) {
- Log.d("GameActivity", "requesting external storage permission", e);
- return false;
- }
- }
- return ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
- }
- @Keep
- public boolean hasRecordAudioPermission() {
- return ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
- }
- @Keep
- public void requestRecordAudioPermission() {
- if (ActivityCompat.checkSelfPermission(this,
- Manifest.permission.RECORD_AUDIO)
- == PackageManager.PERMISSION_GRANTED) {
- return;
- }
- Log.d("GameActivity", "Requesting mic permission and locking LÖVE thread until we have an answer.");
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, RECORD_AUDIO_REQUEST_CODE);
- synchronized (recordAudioRequestDummy) {
- try {
- recordAudioRequestDummy.wait();
- } catch (InterruptedException e) {
- Log.d("GameActivity", "requesting mic permission", e);
- }
- }
- }
- @Keep
- public boolean initializeSafeArea() {
- if (android.os.Build.VERSION.SDK_INT >= 28 && shortEdgesMode) {
- DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
- if (cutout != null) {
- safeAreaTop = cutout.getSafeInsetTop();
- safeAreaLeft = cutout.getSafeInsetLeft();
- safeAreaBottom = cutout.getSafeInsetBottom();
- safeAreaRight = cutout.getSafeInsetRight();
- return true;
- }
- }
- return false;
- }
- @Keep
- public String[] buildFileTree() {
- // Map key is path, value is directory flag
- HashMap<String, Boolean> map = buildFileTree(getAssets(), "", new HashMap<String, Boolean>());
- ArrayList<String> result = new ArrayList<String>();
- for (Map.Entry<String, Boolean> data: map.entrySet()) {
- result.add((data.getValue() ? "d" : "f") + data.getKey());
- }
- String[] r = new String[result.size()];
- result.toArray(r);
- return r;
- }
- private HashMap<String, Boolean> buildFileTree(AssetManager assetManager, String dir, HashMap<String, Boolean> 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("GameActivity", 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("GameActivity", dir, e);
- }
- return map;
- }
- public int getAudioSMP() {
- int smp = 256;
- if (android.os.Build.VERSION.SDK_INT >= 17) {
- AudioManager a = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- int b = Integer.parseInt(a.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER));
- return b > 0 ? b : smp;
- }
- return smp;
- }
- public int getAudioFreq() {
- int freq = 44100;
- if (android.os.Build.VERSION.SDK_INT >= 17) {
- AudioManager a = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- int b = Integer.parseInt(a.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE));
- return b > 0 ? b : freq;
- }
- return freq;
- }
- public boolean isNativeLibsExtracted() {
- ApplicationInfo appInfo = getApplicationInfo();
- if (android.os.Build.VERSION.SDK_INT >= 23) {
- return (appInfo.flags & ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) != 0;
- }
- return true;
- }
- @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;
- if (android.os.Build.VERSION.SDK_INT >= 21) {
- abi = android.os.Build.SUPPORTED_ABIS[0];
- } else {
- // This codepath should NEVER be taken as if isNativeLibsExtracted()
- // returns false, it's 100% safe to assume we're on API level 23 or later.
- abi = android.os.Build.CPU_ABI;
- }
- return applicationInfo.sourceDir + "!/lib/" + abi + "/?.so";
- }
- }
- }
|