From 18bcc929e3b0b66f5028f169cd588eb084895c7e Mon Sep 17 00:00:00 2001 From: cafeed28 Date: Thu, 29 Feb 2024 17:28:41 +0500 Subject: [PATCH] feat: save debug logs to file --- app/build.gradle | 4 +- .../main/java/com/cafeed28/omori/Debug.java | 101 ++++++ .../java/com/cafeed28/omori/MainActivity.java | 11 + .../java/com/cafeed28/omori/NwCompat.java | 302 ++++++++-------- .../cafeed28/omori/NwCompatPathHandler.java | 214 +++++------ .../com/cafeed28/omori/SettingsFragment.java | 341 ++++++++++-------- .../com/cafeed28/omori/WebViewActivity.java | 3 +- .../com/cafeed28/omori/WebViewHelper.java | 286 +++++++-------- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 41 ++- 10 files changed, 729 insertions(+), 576 deletions(-) create mode 100644 app/src/main/java/com/cafeed28/omori/Debug.java diff --git a/app/build.gradle b/app/build.gradle index c5922d4..45db3bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId 'com.cafeed28.omori' minSdk 26 targetSdk 34 - versionCode 4 - versionName '1.2.0' + versionCode 5 + versionName '1.2.1' } buildTypes { diff --git a/app/src/main/java/com/cafeed28/omori/Debug.java b/app/src/main/java/com/cafeed28/omori/Debug.java new file mode 100644 index 0000000..40fd5c2 --- /dev/null +++ b/app/src/main/java/com/cafeed28/omori/Debug.java @@ -0,0 +1,101 @@ +package com.cafeed28.omori; + +import android.content.Context; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.DateFormat; +import java.util.Date; + +public class Debug { + private final String TAG = "omori"; + + private final DateFormat mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); + private final String mInternalFileName; + private final String mExternalFileName = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/omori.log"; + private final PrintWriter mPrintWriter; + + private static Debug mInstance = null; + + public static Debug i() { + return mInstance; + } + + public Debug(String filesPath) throws IOException { + mInternalFileName = filesPath + "/debug.log"; + var fileWriter = new FileWriter(mInternalFileName, true); + var bufferedWriter = new BufferedWriter(fileWriter); + mPrintWriter = new PrintWriter(bufferedWriter); + mPrintWriter.flush(); + mInstance = this; + } + + public void log(int level, String format, Object... args) { + String logLine; + try { + if (args.length > 0) logLine = String.format(format, args); + else logLine = format; + } catch (IllegalArgumentException e) { + logLine = format; + } + + final String levelName; + switch (level) { + case Log.VERBOSE: + Log.v(TAG, logLine); + levelName = "VERBOSE"; + break; + case Log.DEBUG: + Log.d(TAG, logLine); + levelName = "DEBUG"; + break; + case Log.INFO: + Log.i(TAG, logLine); + levelName = "INFO"; + break; + case Log.WARN: + Log.w(TAG, logLine); + levelName = "WARN"; + break; + case Log.ERROR: + Log.e(TAG, logLine); + levelName = "ERROR"; + break; + default: + throw new IllegalArgumentException(); + } + + String dateLine = String.format("[%s] %s: %s", mDateFormat.format(new Date()), levelName, logLine); + mPrintWriter.println(dateLine); + mPrintWriter.flush(); + } + + public void clear(Context context) { + try { + Files.deleteIfExists(Paths.get(mInternalFileName)); + } catch (IOException e) { + Toast.makeText(context, "Failed to clear logs, check 'adb logcat' for more info", Toast.LENGTH_LONG).show(); + this.log(Log.ERROR, "Failed to clear logs from '%s'", mInternalFileName); + e.printStackTrace(); + } + } + + public void save(Context context) { + try { + clear(context); + Files.copy(Paths.get(mInternalFileName), Paths.get(mExternalFileName)); + Toast.makeText(context, String.format("Logs saved in %s", mExternalFileName), Toast.LENGTH_LONG).show(); + } catch (IOException e) { + Toast.makeText(context, "Failed to save logs, check 'adb logcat' for more info", Toast.LENGTH_LONG).show(); + this.log(Log.ERROR, "Failed to save logs to '%s'", mExternalFileName); + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/cafeed28/omori/MainActivity.java b/app/src/main/java/com/cafeed28/omori/MainActivity.java index 9246115..dc51909 100644 --- a/app/src/main/java/com/cafeed28/omori/MainActivity.java +++ b/app/src/main/java/com/cafeed28/omori/MainActivity.java @@ -3,13 +3,24 @@ import android.content.Intent; import android.os.Bundle; import android.widget.Button; +import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceManager; +import java.io.IOException; + public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { + try { + new Debug(getFilesDir().getAbsolutePath()); + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(this, "Fatal error: failed to init logging", Toast.LENGTH_LONG).show(); + finish(); + } + super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); diff --git a/app/src/main/java/com/cafeed28/omori/NwCompat.java b/app/src/main/java/com/cafeed28/omori/NwCompat.java index 26767a5..8da3611 100644 --- a/app/src/main/java/com/cafeed28/omori/NwCompat.java +++ b/app/src/main/java/com/cafeed28/omori/NwCompat.java @@ -1,151 +1,151 @@ -package com.cafeed28.omori; - -import android.util.Log; -import android.webkit.JavascriptInterface; -import android.webkit.WebView; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; - -public class NwCompat { - public static final String INTERFACE = "nwcompat"; - - private final WebView mView; - private final String mDataDirectory; - private final String mGameDirectory; - private final String mKey; - - private final Base64.Decoder mDecoder = Base64.getDecoder(); - private final Base64.Encoder mEncoder = Base64.getEncoder(); - - public NwCompat(WebView view, String dataDirectory, String gameDirectory, String key) { - mView = view; - mDataDirectory = dataDirectory; - mGameDirectory = gameDirectory; - mKey = key; - } - - @JavascriptInterface - public void asyncCall(int id, String methodName, String args) { - NwCompat self = this; - - new Thread(() -> { - try { - JSONObject params = new JSONObject(args); - String result = (String) self.getClass().getMethod(methodName, JSONObject.class).invoke(self, params); - self.jsResolve(id, true, result); - } catch (InvocationTargetException ite) { - self.jsResolve(id, false, ite.getCause().toString()); - } catch (Exception e) { - self.jsResolve(id, false, e.toString()); - } - }).start(); - } - - private void jsResolve(int id, boolean success, String result) { - var formattedResult = result == null ? "null" : String.format("\"%s\"", result); - var code = String.format("nwcompat.async.callback(%d, %b, %s)", id, success, formattedResult); - Log.d("Promise", code); - mView.post(() -> mView.evaluateJavascript(code, null)); - } - - public String fsReadFileAsync(JSONObject args) throws JSONException { - return fsReadFile(args.getString("path")); - } - - @JavascriptInterface - public String getDataDirectory() { - return mDataDirectory; - } - - @JavascriptInterface - public String getGameDirectory() { - return mGameDirectory; - } - - @JavascriptInterface - public String getKey() { - return mKey; - } - - /** - * @return -1: error or file not found; 1: file; 2: directory - */ - @JavascriptInterface - public int fsStat(String path) { - File f = new File(path); - if (f.isFile()) return 1; - else if (f.isDirectory()) return 2; - return -1; - } - - @JavascriptInterface - public String fsReadDir(String path) { - List list = new ArrayList<>(); - - File[] files = new File(path).listFiles(); - if (files != null) { - for (File file : files) { - list.add(file.getName()); - } - } - - return String.join(":", list); - } - - @JavascriptInterface - public void fsMkDir(String path) { - File f = new File(path); - f.mkdir(); - } - - @JavascriptInterface - public String fsReadFile(String path) { - try { - var bytes = Files.readAllBytes(Paths.get(path)); - return mEncoder.encodeToString(bytes); - } catch (IOException e) { - if (!(e instanceof NoSuchFileException)) { - e.printStackTrace(); - } - return null; - } - } - - @JavascriptInterface - public void fsWriteFile(String path, byte[] data) { - try { - Files.write(Paths.get(path), data); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @JavascriptInterface - public void fsUnlink(String path) { - try { - Files.deleteIfExists(Paths.get(path)); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @JavascriptInterface - public void fsRename(String path, String newPath) { - try { - Files.move(Paths.get(path), Paths.get(newPath)); - } catch (IOException e) { - e.printStackTrace(); - } - } -} +package com.cafeed28.omori; + +import android.util.Log; +import android.webkit.JavascriptInterface; +import android.webkit.WebView; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +public class NwCompat { + public static final String INTERFACE = "nwcompat"; + + private final WebView mView; + private final String mDataDirectory; + private final String mGameDirectory; + private final String mKey; + + private final Base64.Decoder mDecoder = Base64.getDecoder(); + private final Base64.Encoder mEncoder = Base64.getEncoder(); + + public NwCompat(WebView view, String dataDirectory, String gameDirectory, String key) { + mView = view; + mDataDirectory = dataDirectory; + mGameDirectory = gameDirectory; + mKey = key; + } + + @JavascriptInterface + public void asyncCall(int id, String methodName, String args) { + NwCompat self = this; + + new Thread(() -> { + try { + JSONObject params = new JSONObject(args); + String result = (String) self.getClass().getMethod(methodName, JSONObject.class).invoke(self, params); + self.jsResolve(id, true, result); + } catch (InvocationTargetException ite) { + self.jsResolve(id, false, ite.getCause().toString()); + } catch (Exception e) { + self.jsResolve(id, false, e.toString()); + } + }).start(); + } + + private void jsResolve(int id, boolean success, String result) { + var formattedResult = result == null ? "null" : String.format("\"%s\"", result); + var code = String.format("nwcompat.async.callback(%d, %b, %s)", id, success, formattedResult); + Debug.i().log(Log.DEBUG, code); + mView.post(() -> mView.evaluateJavascript(code, null)); + } + + public String fsReadFileAsync(JSONObject args) throws JSONException { + return fsReadFile(args.getString("path")); + } + + @JavascriptInterface + public String getDataDirectory() { + return mDataDirectory; + } + + @JavascriptInterface + public String getGameDirectory() { + return mGameDirectory; + } + + @JavascriptInterface + public String getKey() { + return mKey; + } + + /** + * @return -1: error or file not found; 1: file; 2: directory + */ + @JavascriptInterface + public int fsStat(String path) { + File f = new File(path); + if (f.isFile()) return 1; + else if (f.isDirectory()) return 2; + return -1; + } + + @JavascriptInterface + public String fsReadDir(String path) { + List list = new ArrayList<>(); + + File[] files = new File(path).listFiles(); + if (files != null) { + for (File file : files) { + list.add(file.getName()); + } + } + + return String.join(":", list); + } + + @JavascriptInterface + public void fsMkDir(String path) { + File f = new File(path); + f.mkdir(); + } + + @JavascriptInterface + public String fsReadFile(String path) { + try { + var bytes = Files.readAllBytes(Paths.get(path)); + return mEncoder.encodeToString(bytes); + } catch (IOException e) { + if (!(e instanceof NoSuchFileException)) { + e.printStackTrace(); + } + return null; + } + } + + @JavascriptInterface + public void fsWriteFile(String path, byte[] data) { + try { + Files.write(Paths.get(path), data); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @JavascriptInterface + public void fsUnlink(String path) { + try { + Files.deleteIfExists(Paths.get(path)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @JavascriptInterface + public void fsRename(String path, String newPath) { + try { + Files.move(Paths.get(path), Paths.get(newPath)); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/cafeed28/omori/NwCompatPathHandler.java b/app/src/main/java/com/cafeed28/omori/NwCompatPathHandler.java index eee3d58..27a69c9 100644 --- a/app/src/main/java/com/cafeed28/omori/NwCompatPathHandler.java +++ b/app/src/main/java/com/cafeed28/omori/NwCompatPathHandler.java @@ -1,108 +1,108 @@ -package com.cafeed28.omori; - -import android.app.Activity; -import android.util.Log; -import android.webkit.MimeTypeMap; -import android.webkit.WebResourceResponse; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; - -public class NwCompatPathHandler { - private final String TAG = this.getClass().getSimpleName(); - private final Activity mActivity; - private final String mDirectory; - - private static final List mOneLoaderBlockList = Arrays.asList( - "js/libs/pixi.js", - "js/libs/pixi-tilemap.js", - "js/libs/pixi-picture.js" - ); - - public NwCompatPathHandler(Activity activity, String directory) { - mActivity = activity; - mDirectory = directory; - } - - private static String getMimeType(@NonNull String path) { - int lastIndexOf = path.lastIndexOf("."); - String extension = ""; - if (lastIndexOf != -1) { - extension = path.substring(lastIndexOf).toLowerCase(); - } - - return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } - - @Nullable - private InputStream handleGame(String path) { - try { - return Files.newInputStream(Paths.get(mDirectory, path)); - } catch (IOException e) { - if (!(e instanceof NoSuchFileException)) { - e.printStackTrace(); - } - } - return null; - } - - @Nullable - private InputStream handleAsset(String path) { - InputStream is = null; - - if (BuildConfig.DEBUG) { - try { - is = Files.newInputStream(Paths.get(mDirectory, "assets", path)); - } catch (IOException e) { - if (!(e instanceof NoSuchFileException)) { - e.printStackTrace(); - } - } - } - - if (is == null) { - try { - is = mActivity.getAssets().open(path); - } catch (IOException e) { - if (!(e instanceof FileNotFoundException)) { - e.printStackTrace(); - } - } - } - - return is; - } - - public WebResourceResponse handle(String path, boolean oneLoader) { - boolean block = false; - if (oneLoader) { - block = mOneLoaderBlockList.contains(path); - path = path.replace("index.html", "index-oneloader.html"); - } - - InputStream is; - if (block) { - is = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); - } else { - is = handleAsset(path); - if (is == null) is = handleGame(path); - if (is == null) { - Log.d(TAG, String.format("file not found: '%s' ('%s')", path, mDirectory)); - return null; - } - } - - return new WebResourceResponse(getMimeType(path), null, is); - } +package com.cafeed28.omori; + +import android.app.Activity; +import android.util.Log; +import android.webkit.MimeTypeMap; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +public class NwCompatPathHandler { + private final String TAG = this.getClass().getSimpleName(); + private final Activity mActivity; + private final String mDirectory; + + private static final List mOneLoaderBlockList = Arrays.asList( + "js/libs/pixi.js", + "js/libs/pixi-tilemap.js", + "js/libs/pixi-picture.js" + ); + + public NwCompatPathHandler(Activity activity, String directory) { + mActivity = activity; + mDirectory = directory; + } + + private static String getMimeType(@NonNull String path) { + int lastIndexOf = path.lastIndexOf("."); + String extension = ""; + if (lastIndexOf != -1) { + extension = path.substring(lastIndexOf).toLowerCase(); + } + + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + + @Nullable + private InputStream handleGame(String path) { + try { + return Files.newInputStream(Paths.get(mDirectory, path)); + } catch (IOException e) { + if (!(e instanceof NoSuchFileException)) { + e.printStackTrace(); + } + } + return null; + } + + @Nullable + private InputStream handleAsset(String path) { + InputStream is = null; + + if (BuildConfig.DEBUG) { + try { + is = Files.newInputStream(Paths.get(mDirectory, "assets", path)); + } catch (IOException e) { + if (!(e instanceof NoSuchFileException)) { + e.printStackTrace(); + } + } + } + + if (is == null) { + try { + is = mActivity.getAssets().open(path); + } catch (IOException e) { + if (!(e instanceof FileNotFoundException)) { + e.printStackTrace(); + } + } + } + + return is; + } + + public WebResourceResponse handle(String path, boolean oneLoader) { + boolean block = false; + if (oneLoader) { + block = mOneLoaderBlockList.contains(path); + path = path.replace("index.html", "index-oneloader.html"); + } + + InputStream is; + if (block) { + is = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); + } else { + is = handleAsset(path); + if (is == null) is = handleGame(path); + if (is == null) { + Debug.i().log(Log.INFO, "%s: file not found: '%s' ('%s')", TAG, path, mDirectory); + return null; + } + } + + return new WebResourceResponse(getMimeType(path), null, is); + } } \ No newline at end of file diff --git a/app/src/main/java/com/cafeed28/omori/SettingsFragment.java b/app/src/main/java/com/cafeed28/omori/SettingsFragment.java index 1807695..c8fb65f 100644 --- a/app/src/main/java/com/cafeed28/omori/SettingsFragment.java +++ b/app/src/main/java/com/cafeed28/omori/SettingsFragment.java @@ -1,156 +1,185 @@ -package com.cafeed28.omori; - -import android.Manifest; -import android.app.Dialog; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.provider.Settings; -import android.widget.Toast; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import java.nio.file.Files; -import java.nio.file.Paths; - -public class SettingsFragment extends PreferenceFragmentCompat { - public interface OnPreferencesUpdateListener { - void onPreferencesUpdate(SharedPreferences preferences); - } - - private OnPreferencesUpdateListener mListener; - - private final SharedPreferences.OnSharedPreferenceChangeListener prefListener = (preferences, key) -> { - updatePreferences(preferences); - }; - - public static String PREFERENCE_DIRECTORY; - public static String PREFERENCE_KEY; - public static String PREFERENCE_ONELOADER; - - private SharedPreferences mPreferences; - private ActivityResultLauncher mOpenDocumentTree; - private ActivityResultLauncher mRequestPermission; - - private Dialog mOneLoaderDialog; - - public void setOnPreferencesUpdateListener(OnPreferencesUpdateListener listener) { - mListener = listener; - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - PREFERENCE_DIRECTORY = getString(R.string.preference_directory); - PREFERENCE_KEY = getString(R.string.preference_key); - PREFERENCE_ONELOADER = getString(R.string.preference_oneloader); - - mPreferences = PreferenceManager.getDefaultSharedPreferences(context); - mPreferences.registerOnSharedPreferenceChangeListener(prefListener); - - mOpenDocumentTree = registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), uri -> { - if (uri == null) return; - - var uriPath = uri.getPath(); - if (uriPath == null) return; - - String[] pathSections = uriPath.split(":"); - String directory = Environment.getExternalStorageDirectory().getPath() + "/" + pathSections[pathSections.length - 1]; - mPreferences.edit().putString(PREFERENCE_DIRECTORY, directory).apply(); - - updatePreferences(mPreferences); - }); - - mRequestPermission = registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> { - if (!granted) { - Toast.makeText(getContext(), "Storage permission is required", Toast.LENGTH_LONG).show(); - } - }); - - mOneLoaderDialog = new AlertDialog.Builder(getContext()) - .setTitle("OneLoader is not installed") - .setMessage("To use OneLoader, you must install it first") - .setPositiveButton("Install", (d, w) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://mods.one/mod/oneloader")))) - .setNegativeButton("Cancel", (d, w) -> d.cancel()) - .create(); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.preferences, rootKey); - - Preference directoryPreference = findPreference(PREFERENCE_DIRECTORY); - Preference keyPreference = findPreference(PREFERENCE_KEY); - Preference oneLoaderPreference = findPreference(PREFERENCE_ONELOADER); - if (directoryPreference == null || keyPreference == null || oneLoaderPreference == null) return; - - directoryPreference.setOnPreferenceClickListener(preference -> { - if (checkPermissions(getContext())) mOpenDocumentTree.launch(null); - else requestPermissions(); - return true; - }); - - oneLoaderPreference.setOnPreferenceChangeListener((preference, newValue) -> { - if (!isOneLoaderInstalled()) { - mOneLoaderDialog.show(); - return false; - } - return true; - }); - - updatePreferences(mPreferences); - } - - private void updatePreferences(SharedPreferences preferences) { - if (mListener != null) mListener.onPreferencesUpdate(preferences); - - Preference directoryPreference = findPreference(PREFERENCE_DIRECTORY); - Preference oneLoaderPreference = findPreference(PREFERENCE_ONELOADER); - if (directoryPreference == null || oneLoaderPreference == null) return; - - directoryPreference.setSummary(String.format("Current: %s", preferences.getString(PREFERENCE_DIRECTORY, "not set"))); - oneLoaderPreference.setEnabled(canPlay(getContext(), mPreferences)); - } - - private void requestPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Toast.makeText(getContext(), "Allow all files access", Toast.LENGTH_LONG).show(); - startActivity(new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + BuildConfig.APPLICATION_ID))); - } else { - mRequestPermission.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE); - } - } - - private boolean checkPermissions(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return Environment.isExternalStorageManager(); - } else { - return context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - } - } - - public boolean canPlay(Context context, SharedPreferences preferences) { - String directory = preferences.getString(SettingsFragment.PREFERENCE_DIRECTORY, null); - String key = preferences.getString(SettingsFragment.PREFERENCE_KEY, null); - - return directory != null && !directory.isEmpty() && - key != null && !key.isEmpty() && - checkPermissions(context); - } - - private boolean isOneLoaderInstalled() { - var directory = mPreferences.getString(PREFERENCE_DIRECTORY, null); - return Files.exists(Paths.get(directory, "modloader", "early_loader.js")); - } -} +package com.cafeed28.omori; + +import android.Manifest; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.Settings; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import java.nio.file.Files; +import java.nio.file.Paths; + +public class SettingsFragment extends PreferenceFragmentCompat { + public interface OnPreferencesUpdateListener { + void onPreferencesUpdate(SharedPreferences preferences); + } + + private OnPreferencesUpdateListener mListener; + + private final SharedPreferences.OnSharedPreferenceChangeListener prefListener = (preferences, key) -> { + updatePreferences(preferences); + }; + + public static String PREFERENCE_DIRECTORY; + public static String PREFERENCE_KEY; + public static String PREFERENCE_ONELOADER; + public static String PREFERENCE_LOGS; + public static String PREFERENCE_LOGS_CLEAR; + + private SharedPreferences mPreferences; + private ActivityResultLauncher mOpenDocumentTree; + private ActivityResultLauncher mRequestPermission; + + private Dialog mOneLoaderDialog; + + public void setOnPreferencesUpdateListener(OnPreferencesUpdateListener listener) { + mListener = listener; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + PREFERENCE_DIRECTORY = getString(R.string.preference_directory); + PREFERENCE_KEY = getString(R.string.preference_key); + PREFERENCE_ONELOADER = getString(R.string.preference_oneloader); + PREFERENCE_LOGS = getString(R.string.preference_logs); + PREFERENCE_LOGS_CLEAR = getString(R.string.preference_logs_clear); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(context); + mPreferences.registerOnSharedPreferenceChangeListener(prefListener); + + mOpenDocumentTree = registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), uri -> { + if (uri == null) return; + + var uriPath = uri.getPath(); + if (uriPath == null) return; + + String[] pathSections = uriPath.split(":"); + String directory = Environment.getExternalStorageDirectory().getPath() + "/" + pathSections[pathSections.length - 1]; + mPreferences.edit().putString(PREFERENCE_DIRECTORY, directory).apply(); + + updatePreferences(mPreferences); + }); + + mRequestPermission = registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> { + if (!granted) { + Toast.makeText(getContext(), "Storage permission is required", Toast.LENGTH_LONG).show(); + } + updatePreferences(mPreferences); + }); + + mOneLoaderDialog = new AlertDialog.Builder(context) + .setTitle("OneLoader is not installed") + .setMessage("To use OneLoader, you must install it first") + .setPositiveButton("Install", (d, w) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://mods.one/mod/oneloader")))) + .setNegativeButton("Cancel", (d, w) -> d.cancel()) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + updatePreferences(mPreferences); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.preferences, rootKey); + + // @todo: is there a way to do this better? + Preference directoryPreference = findPreference(PREFERENCE_DIRECTORY); + Preference keyPreference = findPreference(PREFERENCE_KEY); + Preference oneLoaderPreference = findPreference(PREFERENCE_ONELOADER); + Preference logsPreference = findPreference(PREFERENCE_LOGS); + Preference logsClearPreference = findPreference(PREFERENCE_LOGS_CLEAR); + if (directoryPreference == null || keyPreference == null || oneLoaderPreference == null || logsPreference == null || logsClearPreference == null) + return; + + directoryPreference.setOnPreferenceClickListener(preference -> { + if (checkPermissions(getContext())) mOpenDocumentTree.launch(null); + else requestPermissions(); + return true; + }); + + logsPreference.setOnPreferenceClickListener(preference -> { + Debug.i().save(getContext()); + return true; + }); + + logsClearPreference.setOnPreferenceClickListener(preference -> { + Debug.i().clear(getContext()); + Toast.makeText(getContext(), "Restart app now", Toast.LENGTH_SHORT).show(); + System.exit(0); + return true; + }); + + oneLoaderPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if (!isOneLoaderInstalled()) { + mOneLoaderDialog.show(); + return false; + } + return true; + }); + + updatePreferences(mPreferences); + } + + private void updatePreferences(SharedPreferences preferences) { + if (mListener != null) mListener.onPreferencesUpdate(preferences); + + Preference directoryPreference = findPreference(PREFERENCE_DIRECTORY); + Preference oneLoaderPreference = findPreference(PREFERENCE_ONELOADER); + Preference logsPreference = findPreference(PREFERENCE_LOGS); + if (directoryPreference == null || oneLoaderPreference == null || logsPreference == null) return; + + directoryPreference.setSummary(String.format("Current: %s", preferences.getString(PREFERENCE_DIRECTORY, "not set"))); + oneLoaderPreference.setEnabled(canPlay(getContext(), mPreferences)); + logsPreference.setEnabled(checkPermissions(getContext())); + } + + private void requestPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Toast.makeText(getContext(), "Allow all files access", Toast.LENGTH_LONG).show(); + startActivity(new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + BuildConfig.APPLICATION_ID))); + } else { + mRequestPermission.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + } + + private boolean checkPermissions(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return Environment.isExternalStorageManager(); + } else { + return context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } + } + + public boolean canPlay(Context context, SharedPreferences preferences) { + String directory = preferences.getString(SettingsFragment.PREFERENCE_DIRECTORY, null); + String key = preferences.getString(SettingsFragment.PREFERENCE_KEY, null); + + return directory != null && !directory.isEmpty() && + key != null && !key.isEmpty() && + checkPermissions(context); + } + + private boolean isOneLoaderInstalled() { + var directory = mPreferences.getString(PREFERENCE_DIRECTORY, null); + return Files.exists(Paths.get(directory, "modloader", "early_loader.js")); + } +} diff --git a/app/src/main/java/com/cafeed28/omori/WebViewActivity.java b/app/src/main/java/com/cafeed28/omori/WebViewActivity.java index 31d8088..8956dfa 100644 --- a/app/src/main/java/com/cafeed28/omori/WebViewActivity.java +++ b/app/src/main/java/com/cafeed28/omori/WebViewActivity.java @@ -1,7 +1,6 @@ package com.cafeed28.omori; import android.app.Activity; -import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.webkit.WebView; @@ -50,7 +49,7 @@ protected void onCreate(Bundle savedInstanceState) { iButton = 3; break; default: - Log.e("Buttons", String.format("Out of range button: %d", button)); + Debug.i().log(Log.ERROR, "Out of range button: %d", button); return; } diff --git a/app/src/main/java/com/cafeed28/omori/WebViewHelper.java b/app/src/main/java/com/cafeed28/omori/WebViewHelper.java index 7184ab5..7b36feb 100644 --- a/app/src/main/java/com/cafeed28/omori/WebViewHelper.java +++ b/app/src/main/java/com/cafeed28/omori/WebViewHelper.java @@ -1,143 +1,143 @@ -package com.cafeed28.omori; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ContextWrapper; -import android.content.SharedPreferences; -import android.net.Uri; -import android.util.Log; -import android.view.View; -import android.webkit.ConsoleMessage; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import androidx.preference.PreferenceManager; - -import java.util.HashMap; -import java.util.Map; - -public class WebViewHelper { - private final WebView mView; - private final Activity mActivity; - - private final String mDataDirectory; - private final String mGameDirectory; - private final String mKey; - private final boolean mOneLoader; - - @SuppressLint("SetJavaScriptEnabled") - public WebViewHelper(WebView view, Activity activity) { - mView = view; - mActivity = activity; - - ContextWrapper contextWrapper = new ContextWrapper(activity); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - - mDataDirectory = contextWrapper.getFilesDir().getPath(); - mGameDirectory = preferences.getString(activity.getString(R.string.preference_directory), null); - mKey = preferences.getString(activity.getString(R.string.preference_key), null); - mOneLoader = preferences.getBoolean(activity.getString(R.string.preference_oneloader), false); - - mView.addJavascriptInterface(new NwCompat(mView, mDataDirectory, mGameDirectory, mKey), NwCompat.INTERFACE); - - mView.setWebViewClient(new ViewClient()); - mView.setWebChromeClient(new ChromeClient()); - - mView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mView.setKeepScreenOn(true); - - WebSettings settings = mView.getSettings(); - - settings.setJavaScriptEnabled(true); - settings.setCacheMode(WebSettings.LOAD_NO_CACHE); - settings.setLoadsImagesAutomatically(true); - settings.setMediaPlaybackRequiresUserGesture(false); - } - - public void start() { - var url = new Uri.Builder() - .scheme("http") - .authority("game") - .appendPath("index.html") - .build() - .toString(); - - Map noCacheHeaders = new HashMap<>(2); - noCacheHeaders.put("Pragma", "no-cache"); - noCacheHeaders.put("Cache-Control", "no-cache"); - - mView.loadUrl(url, noCacheHeaders); - } - - @SuppressLint("DefaultLocale") - public void dispatchButton(int button, boolean pressed) { - String code = String.format("nwcompat.gamepad.buttons[%d].pressed = %b;", button, pressed); - mView.evaluateJavascript(code, null); - } - - @SuppressLint("DefaultLocale") - public void dispatchAxis(int axis, double value) { - String code = String.format("nwcompat.gamepad.axes[%d] = %f", axis, value); - mView.evaluateJavascript(code, null); - } - - private class ViewClient extends WebViewClient { - private final NwCompatPathHandler mPathHandler = new NwCompatPathHandler(mActivity, mGameDirectory); - - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - try { - var path = request.getUrl().getEncodedPath(); - if (path == null) return null; - if (path.charAt(0) == '/') path = path.substring(1); - - if (path.contains("%")) { - var decodedPath = Uri.decode(path); - if (!decodedPath.contains("\0")) path = decodedPath; - } - - return mPathHandler.handle(path, mOneLoader); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - } - - private class ChromeClient extends WebChromeClient { - @Override - public void onCloseWindow(WebView window) { - mActivity.finishAndRemoveTask(); - super.onCloseWindow(window); - } - - @Override - public boolean onConsoleMessage(ConsoleMessage consoleMessage) { - var message = consoleMessage.message(); - - if (BuildConfig.DEBUG) { - switch (consoleMessage.messageLevel()) { - case ERROR: // error - Log.e("Console", message + "\n from " + consoleMessage.sourceId() + ":" + consoleMessage.lineNumber()); - break; - case WARNING: // warn - Log.w("Console", message); - break; - case LOG: // info, log - Log.i("Console", message); - break; - case TIP: // debug - case DEBUG: // ? - Log.d("Console", message); - break; - } - } - - return true; - } - } -} +package com.cafeed28.omori; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ContextWrapper; +import android.content.SharedPreferences; +import android.net.Uri; +import android.util.Log; +import android.view.View; +import android.webkit.ConsoleMessage; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.preference.PreferenceManager; + +import java.util.HashMap; +import java.util.Map; + +public class WebViewHelper { + private final WebView mView; + private final Activity mActivity; + + private final String mDataDirectory; + private final String mGameDirectory; + private final String mKey; + private final boolean mOneLoader; + + @SuppressLint("SetJavaScriptEnabled") + public WebViewHelper(WebView view, Activity activity) { + mView = view; + mActivity = activity; + + ContextWrapper contextWrapper = new ContextWrapper(activity); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + + mDataDirectory = contextWrapper.getFilesDir().getPath(); + mGameDirectory = preferences.getString(activity.getString(R.string.preference_directory), null); + mKey = preferences.getString(activity.getString(R.string.preference_key), null); + mOneLoader = preferences.getBoolean(activity.getString(R.string.preference_oneloader), false); + + mView.addJavascriptInterface(new NwCompat(mView, mDataDirectory, mGameDirectory, mKey), NwCompat.INTERFACE); + + mView.setWebViewClient(new ViewClient()); + mView.setWebChromeClient(new ChromeClient()); + + mView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mView.setKeepScreenOn(true); + + WebSettings settings = mView.getSettings(); + + settings.setJavaScriptEnabled(true); + settings.setCacheMode(WebSettings.LOAD_NO_CACHE); + settings.setLoadsImagesAutomatically(true); + settings.setMediaPlaybackRequiresUserGesture(false); + } + + public void start() { + var url = new Uri.Builder() + .scheme("http") + .authority("game") + .appendPath("index.html") + .build() + .toString(); + + Map noCacheHeaders = new HashMap<>(2); + noCacheHeaders.put("Pragma", "no-cache"); + noCacheHeaders.put("Cache-Control", "no-cache"); + + mView.loadUrl(url, noCacheHeaders); + } + + @SuppressLint("DefaultLocale") + public void dispatchButton(int button, boolean pressed) { + String code = String.format("nwcompat.gamepad.buttons[%d].pressed = %b;", button, pressed); + mView.evaluateJavascript(code, null); + } + + @SuppressLint("DefaultLocale") + public void dispatchAxis(int axis, double value) { + String code = String.format("nwcompat.gamepad.axes[%d] = %f", axis, value); + mView.evaluateJavascript(code, null); + } + + private class ViewClient extends WebViewClient { + private final NwCompatPathHandler mPathHandler = new NwCompatPathHandler(mActivity, mGameDirectory); + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + try { + var path = request.getUrl().getEncodedPath(); + if (path == null) return null; + if (path.charAt(0) == '/') path = path.substring(1); + + if (path.contains("%")) { + var decodedPath = Uri.decode(path); + if (!decodedPath.contains("\0")) path = decodedPath; + } + + return mPathHandler.handle(path, mOneLoader); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + } + + private class ChromeClient extends WebChromeClient { + @Override + public void onCloseWindow(WebView window) { + mActivity.finishAndRemoveTask(); + super.onCloseWindow(window); + } + + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + var logLine = String.format("[CONSOLE]: %s", consoleMessage.message()); + + if (BuildConfig.DEBUG) { + switch (consoleMessage.messageLevel()) { + case ERROR: // error + Debug.i().log(Log.ERROR, "%s\n from %s:%s", logLine, consoleMessage.sourceId(), consoleMessage.lineNumber()); + break; + case WARNING: // warn + Debug.i().log(Log.WARN, logLine); + break; + case LOG: // info, log + Debug.i().log(Log.INFO, logLine); + break; + case TIP: // debug + case DEBUG: // ? + Debug.i().log(Log.DEBUG, logLine); + break; + } + } + + return true; + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 216a34a..2fd55c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,6 @@ key directory oneloader + logs + logs_clear \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 09cb68d..6564c08 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,16 +1,27 @@ - - - - - + + + + + + + + + + + \ No newline at end of file