MainActivity.java 7.6 KB

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