MainActivity.java 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. /*
  2. * Copyright (c) 2006-2024 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 android.annotation.SuppressLint;
  22. import android.content.Intent;
  23. import android.net.Uri;
  24. import android.os.Build;
  25. import android.os.Bundle;
  26. import android.util.Log;
  27. import android.view.Menu;
  28. import android.view.MenuInflater;
  29. import android.view.MenuItem;
  30. import android.view.View;
  31. import androidx.activity.result.ActivityResultLauncher;
  32. import androidx.activity.result.contract.ActivityResultContracts;
  33. import androidx.appcompat.app.AlertDialog;
  34. import androidx.appcompat.app.AppCompatActivity;
  35. import androidx.constraintlayout.widget.ConstraintLayout;
  36. import androidx.recyclerview.widget.LinearLayoutManager;
  37. import androidx.recyclerview.widget.RecyclerView;
  38. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
  39. import java.io.File;
  40. import java.io.IOException;
  41. import java.util.ArrayList;
  42. import java.util.concurrent.Executor;
  43. import java.util.concurrent.Executors;
  44. import java.util.zip.ZipFile;
  45. public class MainActivity extends AppCompatActivity {
  46. private static final String TAG = "MainActivity";
  47. private final Executor executor = Executors.newSingleThreadExecutor();
  48. private final ActivityResultLauncher<String[]> openFileLauncher = registerForActivityResult(
  49. new ActivityResultContracts.OpenDocument(),
  50. (Uri result) -> {
  51. if (result != null) {
  52. Intent intent = new Intent(this, GameActivity.class);
  53. intent.setData(result);
  54. startActivity(intent);
  55. }
  56. }
  57. );
  58. @Override
  59. protected void onCreate(Bundle savedInstanceState) {
  60. super.onCreate(savedInstanceState);
  61. setContentView(R.layout.activity_main);
  62. RecyclerView recyclerView = findViewById(R.id.recyclerView);
  63. SwipeRefreshLayout swipeLayout = findViewById(R.id.swipeRefreshLayout);
  64. ConstraintLayout noGameText = findViewById(R.id.constraintLayout);
  65. GameListAdapter adapter = new GameListAdapter();
  66. // Set refresh listener
  67. swipeLayout.setOnRefreshListener(() -> {
  68. scanGames(adapter, noGameText, swipeLayout);
  69. });
  70. // Set layout manager and adapter
  71. recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
  72. recyclerView.setAdapter(adapter);
  73. scanGames(adapter, noGameText, null);
  74. }
  75. @Override
  76. public boolean onCreateOptionsMenu(Menu menu) {
  77. MenuInflater inflater = getMenuInflater();
  78. inflater.inflate(R.menu.options_menu, menu);
  79. return true;
  80. }
  81. @Override
  82. public boolean onOptionsItemSelected(MenuItem item) {
  83. int itemId = item.getItemId();
  84. // Handle item selection
  85. if (itemId == R.id.optionItem) {
  86. openFileLauncher.launch(new String[]{"*/*"});
  87. return true;
  88. } else if (itemId == R.id.optionItem2) {
  89. Intent intent = new Intent(this, GameActivity.class);
  90. startActivity(intent);
  91. return true;
  92. } else if (itemId == R.id.optionItem3) {
  93. getGameFolderDialog().show();
  94. return true;
  95. } else if (itemId == R.id.optionItem4) {
  96. Intent intent = new Intent(this, AboutActivity.class);
  97. startActivity(intent);
  98. return true;
  99. } else {
  100. return super.onOptionsItemSelected(item);
  101. }
  102. }
  103. private AlertDialog getGameFolderDialog() {
  104. AlertDialog.Builder builder = new AlertDialog.Builder(this)
  105. .setTitle(getString(R.string.game_folder))
  106. .setPositiveButton(R.string.ok, (dialog1, which) -> {
  107. });
  108. StringBuilder message = new StringBuilder()
  109. .append(getString(R.string.game_folder_location, getPackageName()))
  110. .append("\n\n");
  111. if (Build.VERSION.SDK_INT >= 30) {
  112. message.append(getString(R.string.game_folder_inaccessible));
  113. } else {
  114. message.append(getString(R.string.game_folder_accessible));
  115. }
  116. return builder.setMessage(message.toString()).create();
  117. }
  118. @SuppressLint("NotifyDataSetChanged")
  119. private void scanGames(GameListAdapter adapter, ConstraintLayout noGameText, SwipeRefreshLayout swipeRefreshLayout) {
  120. executor.execute(() -> {
  121. File extDir = getExternalFilesDir("games");
  122. if (extDir != null) {
  123. if (!extDir.isDirectory()) {
  124. if (!extDir.mkdir()) {
  125. // Scan failure, abort
  126. runOnUiThread(() -> {
  127. if (swipeRefreshLayout != null) {
  128. swipeRefreshLayout.setRefreshing(false);
  129. }
  130. });
  131. Log.e(TAG, "Directory creation failure.");
  132. return;
  133. }
  134. }
  135. ArrayList<GameListAdapter.Data> validGames = new ArrayList<>();
  136. File[] files = extDir.listFiles();
  137. if (files != null) {
  138. for (File file : files) {
  139. GameListAdapter.Data gameData = null;
  140. if (file.isDirectory()) {
  141. if (isValidGamedir(file)) {
  142. gameData = new GameListAdapter.Data();
  143. gameData.path = file;
  144. gameData.directory = true;
  145. }
  146. } else {
  147. if (isValidLovegame(file)) {
  148. gameData = new GameListAdapter.Data();
  149. gameData.path = file;
  150. gameData.directory = false;
  151. }
  152. }
  153. if (gameData != null) {
  154. validGames.add(gameData);
  155. }
  156. }
  157. }
  158. boolean empty = validGames.isEmpty();
  159. runOnUiThread(() -> {
  160. if (empty) {
  161. adapter.setData(null);
  162. } else {
  163. GameListAdapter.Data[] gameDatas = new GameListAdapter.Data[validGames.size()];
  164. validGames.toArray(gameDatas);
  165. adapter.setData(gameDatas);
  166. }
  167. adapter.notifyDataSetChanged();
  168. if (swipeRefreshLayout != null) {
  169. swipeRefreshLayout.setRefreshing(false);
  170. }
  171. noGameText.setVisibility(empty ? View.VISIBLE : View.INVISIBLE);
  172. });
  173. }
  174. });
  175. }
  176. public static boolean isValidLovegame(File file) {
  177. boolean valid = false;
  178. try {
  179. ZipFile zip = new ZipFile(file, ZipFile.OPEN_READ);
  180. valid = zip.getEntry("main.lua") != null;
  181. zip.close();
  182. } catch (IOException ignored) {
  183. }
  184. return valid;
  185. }
  186. public static boolean isValidGamedir(File file) {
  187. File mainLua = new File(file, "main.lua");
  188. return mainLua.isFile();
  189. }
  190. }