diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 22d9867e..3e722f9d 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -114,7 +114,6 @@
-
diff --git a/app/build.gradle b/app/build.gradle
index 31b782c2..a8968dfe 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,4 +1,6 @@
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
@@ -13,6 +15,9 @@ android {
abiFilters "arm64-v8a"
}
}
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
buildTypes {
release {
debuggable true
@@ -51,4 +56,9 @@ dependencies {
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'me.xdrop:fuzzywuzzy:1.2.0'
+ implementation "androidx.core:core-ktx:1.1.0"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
+repositories {
+ mavenCentral()
}
diff --git a/app/src/main/java/emu/skyline/GameActivity.kt b/app/src/main/java/emu/skyline/GameActivity.kt
new file mode 100644
index 00000000..717cb784
--- /dev/null
+++ b/app/src/main/java/emu/skyline/GameActivity.kt
@@ -0,0 +1,88 @@
+package emu.skyline
+
+import android.net.Uri
+import android.os.Bundle
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import android.view.InputQueue
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.WindowManager
+import androidx.appcompat.app.AppCompatActivity
+import kotlinx.android.synthetic.main.game_activity.*
+import java.io.File
+import java.lang.reflect.Method
+
+class GameActivity : AppCompatActivity(), SurfaceHolder.Callback, InputQueue.Callback {
+ init {
+ System.loadLibrary("skyline") // libskyline.so
+ }
+
+ private lateinit var rom: Uri
+ private lateinit var romFd: ParcelFileDescriptor
+ private lateinit var preferenceFd: ParcelFileDescriptor
+ private lateinit var logFd: ParcelFileDescriptor
+ private var surface: Surface? = null
+ private var inputQueue: Long? = null
+ private lateinit var gameThread: Thread
+ private var halt: Boolean = false
+
+ private external fun executeRom(romString: String, romType: Int, romFd: Int, preferenceFd: Int, logFd: Int)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.game_activity)
+ rom = intent.getParcelableExtra("romUri")!!
+ val romType = intent.getIntExtra("romType", 0)
+ romFd = contentResolver.openFileDescriptor(rom, "r")!!
+ val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml")
+ preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE)
+ val log = File("${applicationInfo.dataDir}/skyline.log")
+ logFd = ParcelFileDescriptor.open(log, ParcelFileDescriptor.MODE_READ_WRITE)
+ window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ game_view.holder.addCallback(this)
+ //window.takeInputQueue(this)
+ gameThread = Thread {
+ while ((surface == null))
+ Thread.yield()
+ executeRom(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, logFd.fd)
+ runOnUiThread { finish() }
+ }
+ gameThread.start()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ halt = true
+ gameThread.join()
+ romFd.close()
+ preferenceFd.close()
+ logFd.close()
+ }
+
+ override fun surfaceCreated(holder: SurfaceHolder?) {
+ Log.d("surfaceCreated", "Holder: ${holder.toString()}")
+ surface = holder!!.surface
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
+ Log.d("surfaceChanged", "Holder: ${holder.toString()}, Format: $format, Width: $width, Height: $height")
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder?) {
+ Log.d("surfaceDestroyed", "Holder: ${holder.toString()}")
+ surface = null
+ }
+
+ override fun onInputQueueCreated(queue: InputQueue?) {
+ Log.i("onInputQueueCreated", "InputQueue: ${queue.toString()}")
+ val clazz = Class.forName("android.view.InputQueue")
+ val method: Method = clazz.getMethod("getNativePtr")
+ inputQueue = method.invoke(queue)!! as Long
+ }
+
+ override fun onInputQueueDestroyed(queue: InputQueue?) {
+ Log.d("onInputQueueDestroyed", "InputQueue: ${queue.toString()}")
+ inputQueue = null
+ }
+}
diff --git a/app/src/main/java/emu/skyline/GameAdapter.java b/app/src/main/java/emu/skyline/GameAdapter.java
deleted file mode 100644
index 2e252800..00000000
--- a/app/src/main/java/emu/skyline/GameAdapter.java
+++ /dev/null
@@ -1,150 +0,0 @@
-package emu.skyline;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Objects;
-
-class GameItem extends BaseItem {
- private final File file;
- private final int index;
- private transient TitleEntry meta;
-
- GameItem(final File file) {
- this.file = file;
- index = file.getName().lastIndexOf(".");
- meta = NroLoader.getTitleEntry(getPath());
- if (meta == null) {
- meta = new TitleEntry(file.getName(), HeaderAdapter.mContext.getString(R.string.aset_missing), null);
- }
- }
-
- public boolean hasIcon() {
- return !getSubTitle().equals(HeaderAdapter.mContext.getString(R.string.aset_missing));
- }
-
- public Bitmap getIcon() {
- return meta.getIcon();
- }
-
- public String getTitle() {
- return meta.getName() + " (" + getType() + ")";
- }
-
- String getSubTitle() {
- return meta.getAuthor();
- }
-
- private String getType() {
- return file.getName().substring(index + 1).toUpperCase();
- }
-
- public File getFile() {
- return file;
- }
-
- public String getPath() {
- return file.getAbsolutePath();
- }
-
- @Override
- String key() {
- if (meta.getIcon() == null)
- return meta.getName();
- return meta.getName() + " " + meta.getAuthor();
- }
-}
-
-public class GameAdapter extends HeaderAdapter implements View.OnClickListener {
-
- GameAdapter(final Context context) { super(context); }
-
- @Override
- public void load(final File file) throws IOException, ClassNotFoundException {
- super.load(file);
- for (int i = 0; i < item_array.size(); i++)
- item_array.set(i, new GameItem(item_array.get(i).getFile()));
- notifyDataSetChanged();
- }
-
- @Override
- public void onClick(final View view) {
- final int position = (int) view.getTag();
- if (getItemViewType(position) == ContentType.Item) {
- final GameItem item = (GameItem) getItem(position);
- if (view.getId() == R.id.icon) {
- final Dialog builder = new Dialog(HeaderAdapter.mContext);
- builder.requestWindowFeature(Window.FEATURE_NO_TITLE);
- Objects.requireNonNull(builder.getWindow()).setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
- final ImageView imageView = new ImageView(HeaderAdapter.mContext);
- assert item != null;
- imageView.setImageBitmap(item.getIcon());
- builder.addContentView(imageView, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
- builder.show();
- }
- }
- }
-
- @NonNull
- @Override
- public View getView(final int position, View convertView, @NonNull final ViewGroup parent) {
- final GameAdapter.ViewHolder viewHolder;
- final int type = type_array.get(position).type;
- if (convertView == null) {
- if (type == ContentType.Item) {
- viewHolder = new GameAdapter.ViewHolder();
- final LayoutInflater inflater = LayoutInflater.from(HeaderAdapter.mContext);
- convertView = inflater.inflate(R.layout.game_item, parent, false);
- viewHolder.icon = convertView.findViewById(R.id.icon);
- viewHolder.txtTitle = convertView.findViewById(R.id.text_title);
- viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle);
- convertView.setTag(viewHolder);
- } else {
- viewHolder = new GameAdapter.ViewHolder();
- final LayoutInflater inflater = LayoutInflater.from(HeaderAdapter.mContext);
- convertView = inflater.inflate(R.layout.section_item, parent, false);
- viewHolder.txtTitle = convertView.findViewById(R.id.text_title);
- convertView.setTag(viewHolder);
- }
- } else {
- viewHolder = (GameAdapter.ViewHolder) convertView.getTag();
- }
- if (type == ContentType.Item) {
- final GameItem data = (GameItem) getItem(position);
- viewHolder.txtTitle.setText(data.getTitle());
- viewHolder.txtSub.setText(data.getSubTitle());
- final Bitmap icon = data.getIcon();
- if (icon != null) {
- viewHolder.icon.setImageBitmap(icon);
- viewHolder.icon.setOnClickListener(this);
- viewHolder.icon.setTag(position);
- } else {
- viewHolder.icon.setImageDrawable(HeaderAdapter.mContext.getDrawable(R.drawable.ic_missing_icon));
- viewHolder.icon.setOnClickListener(null);
- }
- } else {
- viewHolder.txtTitle.setText((String) getItem(position));
- }
- return convertView;
- }
-
- private static class ViewHolder {
- ImageView icon;
- TextView txtTitle;
- TextView txtSub;
- }
-}
diff --git a/app/src/main/java/emu/skyline/HeaderAdapter.java b/app/src/main/java/emu/skyline/HeaderAdapter.java
deleted file mode 100644
index 0d50b08d..00000000
--- a/app/src/main/java/emu/skyline/HeaderAdapter.java
+++ /dev/null
@@ -1,197 +0,0 @@
-package emu.skyline;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.util.SparseIntArray;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.Filter;
-import android.widget.Filterable;
-
-import androidx.annotation.NonNull;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-import java.util.ArrayList;
-
-import me.xdrop.fuzzywuzzy.FuzzySearch;
-import me.xdrop.fuzzywuzzy.model.ExtractedResult;
-
-class ContentType implements Serializable {
- static final transient int Header = 0;
- static final transient int Item = 1;
- public final int type;
- public int index;
-
- ContentType(final int index, final int type) {
- this(type);
- this.index = index;
- }
-
- private ContentType(final int type) {
- switch (type) {
- case ContentType.Item:
- case ContentType.Header:
- break;
- default:
- throw (new IllegalArgumentException());
- }
- this.type = type;
- }
-}
-
-abstract class BaseItem implements Serializable {
- abstract String key();
-}
-
-abstract class HeaderAdapter extends BaseAdapter implements Filterable, Serializable {
- @SuppressLint("StaticFieldLeak")
- static Context mContext;
- ArrayList type_array;
- ArrayList item_array;
- private ArrayList type_array_uf;
- private ArrayList header_array;
- private String search_term = "";
-
- HeaderAdapter(final Context context) {
- HeaderAdapter.mContext = context;
- item_array = new ArrayList<>();
- header_array = new ArrayList<>();
- type_array_uf = new ArrayList<>();
- type_array = new ArrayList<>();
- }
-
- public void add(final Object item, final int type) {
- if (type == ContentType.Item) {
- item_array.add((ItemType) item);
- type_array_uf.add(new ContentType(item_array.size() - 1, ContentType.Item));
- } else {
- header_array.add((String) item);
- type_array_uf.add(new ContentType(header_array.size() - 1, ContentType.Header));
- }
- if (search_term.length() != 0)
- getFilter().filter(search_term);
- else
- type_array = type_array_uf;
- }
-
- public void save(final File file) throws IOException {
- final HeaderAdapter.State state = new HeaderAdapter.State(item_array, header_array, type_array_uf);
- final FileOutputStream file_obj = new FileOutputStream(file);
- final ObjectOutputStream out = new ObjectOutputStream(file_obj);
- out.writeObject(state);
- out.close();
- file_obj.close();
- }
-
- void load(final File file) throws IOException, ClassNotFoundException {
- final FileInputStream file_obj = new FileInputStream(file);
- final ObjectInputStream in = new ObjectInputStream(file_obj);
- final HeaderAdapter.State state = (HeaderAdapter.State) in.readObject();
- in.close();
- file_obj.close();
- if (state != null) {
- item_array = state.item_array;
- header_array = state.header_array;
- type_array_uf = state.type_array;
- getFilter().filter(search_term);
- }
- }
-
- public void clear() {
- item_array.clear();
- header_array.clear();
- type_array_uf.clear();
- type_array.clear();
- notifyDataSetChanged();
- }
-
- @Override
- public int getCount() {
- return type_array.size();
- }
-
- @Override
- public Object getItem(final int i) {
- final ContentType type = type_array.get(i);
- if (type.type == ContentType.Item)
- return item_array.get(type.index);
- else
- return header_array.get(type.index);
- }
-
- @Override
- public long getItemId(final int position) {
- return position;
- }
-
- @Override
- public int getItemViewType(final int position) {
- return type_array.get(position).type;
- }
-
- @Override
- public int getViewTypeCount() {
- return 2;
- }
-
- @NonNull
- @Override
- public abstract View getView(int position, View convertView, @NonNull ViewGroup parent);
-
- @Override
- public Filter getFilter() {
- return new Filter() {
- @Override
- protected Filter.FilterResults performFiltering(final CharSequence charSequence) {
- final Filter.FilterResults results = new Filter.FilterResults();
- search_term = ((String) charSequence).toLowerCase().replaceAll(" ", "");
- if (charSequence.length() == 0) {
- results.values = type_array_uf;
- results.count = type_array_uf.size();
- } else {
- final ArrayList filter_data = new ArrayList<>();
- final ArrayList key_arr = new ArrayList<>();
- final SparseIntArray key_ind = new SparseIntArray();
- for (int index = 0; index < type_array_uf.size(); index++) {
- final ContentType item = type_array_uf.get(index);
- if (item.type == ContentType.Item) {
- key_arr.add(item_array.get(item.index).key().toLowerCase());
- key_ind.append(key_arr.size() - 1, index);
- }
- }
- for (final ExtractedResult result : FuzzySearch.extractTop(search_term, key_arr, Math.max(1, 10 - search_term.length())))
- if (result.getScore() >= 35)
- filter_data.add(type_array_uf.get(key_ind.get(result.getIndex())));
- results.values = filter_data;
- results.count = filter_data.size();
- }
- return results;
- }
-
- @Override
- protected void publishResults(final CharSequence charSequence, final Filter.FilterResults filterResults) {
- type_array = (ArrayList) filterResults.values;
- notifyDataSetChanged();
- }
- };
- }
-
- class State implements Serializable {
- private final ArrayList item_array;
- private final ArrayList header_array;
- private final ArrayList type_array;
-
- State(final ArrayList item_array, final ArrayList header_array, final ArrayList type_array) {
- this.item_array = item_array;
- this.header_array = header_array;
- this.type_array = type_array;
- }
- }
-}
diff --git a/app/src/main/java/emu/skyline/LogActivity.java b/app/src/main/java/emu/skyline/LogActivity.java
deleted file mode 100644
index 076283bb..00000000
--- a/app/src/main/java/emu/skyline/LogActivity.java
+++ /dev/null
@@ -1,166 +0,0 @@
-package emu.skyline;
-
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.widget.ListView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.SearchView;
-import androidx.preference.PreferenceManager;
-
-import org.json.JSONObject;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.stream.Collectors;
-
-import javax.net.ssl.HttpsURLConnection;
-
-public class LogActivity extends AppCompatActivity {
- private File log_file;
- private LogAdapter adapter;
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.log_activity);
- setSupportActionBar(findViewById(R.id.toolbar));
- final ActionBar actionBar = getSupportActionBar();
- if (actionBar != null)
- actionBar.setDisplayHomeAsUpEnabled(true);
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- ListView log_list = findViewById(R.id.log_list);
- adapter = new LogAdapter(this, prefs.getBoolean("log_compact", false), Integer.parseInt(prefs.getString("log_level", "3")), getResources().getStringArray(R.array.log_level));
- log_list.setAdapter(adapter);
- try {
- log_file = new File(getApplicationInfo().dataDir + "/skyline.log");
- final InputStream inputStream = new FileInputStream(log_file);
- final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
- try {
- boolean done = false;
- while (!done) {
- String line = reader.readLine();
- if (!(done = (line == null))) {
- adapter.add(line);
- }
- }
- } catch (final IOException e) {
- Log.w("Logger", "IO Error during access of log file: " + e.getMessage());
- Toast.makeText(getApplicationContext(), getString(R.string.io_error) + ": " + e.getMessage(), Toast.LENGTH_LONG).show();
- }
- } catch (final FileNotFoundException e) {
- Log.w("Logger", "IO Error during access of log file: " + e.getMessage());
- Toast.makeText(getApplicationContext(), getString(R.string.file_missing), Toast.LENGTH_LONG).show();
- finish();
- }
- }
-
- @Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- getMenuInflater().inflate(R.menu.toolbar_log, menu);
- final MenuItem mSearch = menu.findItem(R.id.action_search_log);
- SearchView searchView = (SearchView) mSearch.getActionView();
- searchView.setSubmitButtonEnabled(false);
- searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
- public boolean onQueryTextSubmit(final String query) {
- searchView.setIconified(false);
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(final String newText) {
- adapter.getFilter().filter(newText);
- return true;
- }
- });
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_clear:
- try {
- final FileWriter fileWriter = new FileWriter(log_file, false);
- fileWriter.close();
- } catch (final IOException e) {
- Log.w("Logger", "IO Error while clearing the log file: " + e.getMessage());
- Toast.makeText(getApplicationContext(), getString(R.string.io_error) + ": " + e.getMessage(), Toast.LENGTH_LONG).show();
- }
- Toast.makeText(getApplicationContext(), getString(R.string.cleared), Toast.LENGTH_LONG).show();
- finish();
- return true;
- case R.id.action_share_log:
- final Thread share_thread = new Thread(() -> {
- HttpsURLConnection urlConnection = null;
- try {
- final URL url = new URL("https://hastebin.com/documents");
- urlConnection = (HttpsURLConnection) url.openConnection();
- urlConnection.setRequestMethod("POST");
- urlConnection.setRequestProperty("Host", "hastebin.com");
- urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
- urlConnection.setRequestProperty("Referer", "https://hastebin.com/");
- urlConnection.setRequestProperty("Connection", "keep-alive");
- final OutputStream outputStream = new BufferedOutputStream(urlConnection.getOutputStream());
- final BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
- final FileReader fileReader = new FileReader(log_file);
- int chr;
- while ((chr = fileReader.read()) != -1) {
- bufferedWriter.write(chr);
- }
- bufferedWriter.flush();
- bufferedWriter.close();
- outputStream.close();
- if (urlConnection.getResponseCode() != 200) {
- Log.e("LogUpload", "HTTPS Status Code: " + urlConnection.getResponseCode());
- throw new Exception();
- }
- final InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());
- final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
- final String key = new JSONObject(bufferedReader.lines().collect(Collectors.joining())).getString("key");
- bufferedReader.close();
- inputStream.close();
- final String result = "https://hastebin.com/" + key;
- final Intent sharingIntent = new Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, result);
- startActivity(Intent.createChooser(sharingIntent, "Share log url with:"));
- } catch (final Exception e) {
- runOnUiThread(() -> Toast.makeText(getApplicationContext(), getString(R.string.share_error), Toast.LENGTH_LONG).show());
- e.printStackTrace();
- } finally {
- assert urlConnection != null;
- urlConnection.disconnect();
- }
- });
- share_thread.start();
- try {
- share_thread.join(1000);
- } catch (final Exception e) {
- Toast.makeText(getApplicationContext(), getString(R.string.share_error), Toast.LENGTH_LONG).show();
- e.printStackTrace();
- }
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-}
diff --git a/app/src/main/java/emu/skyline/LogActivity.kt b/app/src/main/java/emu/skyline/LogActivity.kt
new file mode 100644
index 00000000..d8ccacf1
--- /dev/null
+++ b/app/src/main/java/emu/skyline/LogActivity.kt
@@ -0,0 +1,144 @@
+package emu.skyline
+
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.ListView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
+import androidx.preference.PreferenceManager
+import emu.skyline.adapter.LogAdapter
+import org.json.JSONObject
+import java.io.*
+import java.net.URL
+import java.nio.charset.StandardCharsets
+import java.util.stream.Collectors
+import javax.net.ssl.HttpsURLConnection
+
+class LogActivity : AppCompatActivity() {
+ private lateinit var logFile: File
+ private lateinit var adapter: LogAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.log_activity)
+ setSupportActionBar(findViewById(R.id.toolbar))
+ val actionBar = supportActionBar
+ actionBar?.setDisplayHomeAsUpEnabled(true)
+ val prefs = PreferenceManager.getDefaultSharedPreferences(this)
+ val logList = findViewById(R.id.log_list)
+ adapter = LogAdapter(this, prefs.getBoolean("log_compact", false), prefs.getString("log_level", "3")!!.toInt(), resources.getStringArray(R.array.log_level))
+ logList.adapter = adapter
+ try {
+ logFile = File(applicationInfo.dataDir + "/skyline.log")
+ val inputStream: InputStream = FileInputStream(logFile)
+ val reader = BufferedReader(InputStreamReader(inputStream))
+ try {
+ var done = false
+ while (!done) {
+ val line = reader.readLine()
+ if (!(line == null).also { done = it }) {
+ adapter.add(line)
+ }
+ }
+ } catch (e: IOException) {
+ Log.w("Logger", "IO Error during access of log file: " + e.message)
+ Toast.makeText(applicationContext, getString(R.string.io_error) + ": " + e.message, Toast.LENGTH_LONG).show()
+ }
+ } catch (e: FileNotFoundException) {
+ Log.w("Logger", "IO Error during access of log file: " + e.message)
+ Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show()
+ finish()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.toolbar_log, menu)
+ val mSearch = menu.findItem(R.id.action_search_log)
+ val searchView = mSearch.actionView as SearchView
+ searchView.isSubmitButtonEnabled = false
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ searchView.isIconified = false
+ return false
+ }
+
+ override fun onQueryTextChange(newText: String): Boolean {
+ adapter.filter.filter(newText)
+ return true
+ }
+ })
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.action_clear -> {
+ try {
+ val fileWriter = FileWriter(logFile, false)
+ fileWriter.close()
+ } catch (e: IOException) {
+ Log.w("Logger", "IO Error while clearing the log file: " + e.message)
+ Toast.makeText(applicationContext, getString(R.string.io_error) + ": " + e.message, Toast.LENGTH_LONG).show()
+ }
+ Toast.makeText(applicationContext, getString(R.string.cleared), Toast.LENGTH_LONG).show()
+ finish()
+ true
+ }
+ R.id.action_share_log -> {
+ val shareThread = Thread(Runnable {
+ var urlConnection: HttpsURLConnection? = null
+ try {
+ val url = URL("https://hastebin.com/documents")
+ urlConnection = url.openConnection() as HttpsURLConnection
+ urlConnection.requestMethod = "POST"
+ urlConnection.setRequestProperty("Host", "hastebin.com")
+ urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8")
+ urlConnection.setRequestProperty("Referer", "https://hastebin.com/")
+ urlConnection.setRequestProperty("Connection", "keep-alive")
+ val outputStream: OutputStream = BufferedOutputStream(urlConnection.outputStream)
+ val bufferedWriter = BufferedWriter(OutputStreamWriter(outputStream, StandardCharsets.UTF_8))
+ val fileReader = FileReader(logFile)
+ var chr: Int
+ while (fileReader.read().also { chr = it } != -1) {
+ bufferedWriter.write(chr)
+ }
+ bufferedWriter.flush()
+ bufferedWriter.close()
+ outputStream.close()
+ if (urlConnection.responseCode != 200) {
+ Log.e("LogUpload", "HTTPS Status Code: " + urlConnection.responseCode)
+ throw Exception()
+ }
+ val inputStream: InputStream = BufferedInputStream(urlConnection.inputStream)
+ val bufferedReader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
+ val key = JSONObject(bufferedReader.lines().collect(Collectors.joining())).getString("key")
+ bufferedReader.close()
+ inputStream.close()
+ val result = "https://hastebin.com/$key"
+ val sharingIntent = Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, result)
+ startActivity(Intent.createChooser(sharingIntent, "Share log url with:"))
+ } catch (e: Exception) {
+ runOnUiThread { Toast.makeText(applicationContext, getString(R.string.share_error), Toast.LENGTH_LONG).show() }
+ e.printStackTrace()
+ } finally {
+ assert(urlConnection != null)
+ urlConnection!!.disconnect()
+ }
+ })
+ shareThread.start()
+ try {
+ shareThread.join(1000)
+ } catch (e: Exception) {
+ Toast.makeText(applicationContext, getString(R.string.share_error), Toast.LENGTH_LONG).show()
+ e.printStackTrace()
+ }
+ super.onOptionsItemSelected(item)
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+}
diff --git a/app/src/main/java/emu/skyline/LogAdapter.java b/app/src/main/java/emu/skyline/LogAdapter.java
deleted file mode 100644
index 9b741caa..00000000
--- a/app/src/main/java/emu/skyline/LogAdapter.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package emu.skyline;
-
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-
-class LogItem extends BaseItem {
- private final String content;
- private final String level;
-
- LogItem(final String content, final String level) {
- this.content = content;
- this.level = level;
- }
-
- public String getLevel() {
- return level;
- }
-
- public String getMessage() {
- return content;
- }
-
- @Override
- String key() {
- return getMessage();
- }
-}
-
-public class LogAdapter extends HeaderAdapter implements View.OnLongClickListener {
- private final ClipboardManager clipboard;
- private final int debug_level;
- private final String[] level_str;
- private final boolean compact;
-
- LogAdapter(final Context context, final boolean compact, final int debug_level, final String[] level_str) {
- super(context);
- clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
- this.debug_level = debug_level;
- this.level_str = level_str;
- this.compact = compact;
- }
-
- void add(String log_line) {
- try {
- final String[] log_meta = log_line.split("\\|", 3);
- if (log_meta[0].startsWith("1")) {
- final int level = Integer.parseInt(log_meta[1]);
- if (level > debug_level) return;
- add(new LogItem(log_meta[2].replace('\\', '\n'), level_str[level]), ContentType.Item);
- } else {
- add(log_meta[1], ContentType.Header);
- }
- } catch (final IndexOutOfBoundsException ignored) {}
- }
-
- @Override
- public boolean onLongClick(final View view) {
- final LogItem item = (LogItem) getItem(((LogAdapter.ViewHolder) view.getTag()).position);
- clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.getMessage() + " (" + item.getLevel() + ")"));
- Toast.makeText(view.getContext(), "Copied to clipboard", Toast.LENGTH_LONG).show();
- return false;
- }
-
- @NonNull
- @Override
- public View getView(final int position, View convertView, @NonNull final ViewGroup parent) {
- final LogAdapter.ViewHolder viewHolder;
- final int type = type_array.get(position).type;
- if (convertView == null) {
- viewHolder = new LogAdapter.ViewHolder();
- final LayoutInflater inflater = LayoutInflater.from(HeaderAdapter.mContext);
- if (type == ContentType.Item) {
- if (compact) {
- convertView = inflater.inflate(R.layout.log_item_compact, parent, false);
- viewHolder.txtTitle = convertView.findViewById(R.id.text_title);
- } else {
- convertView = inflater.inflate(R.layout.log_item, parent, false);
- viewHolder.txtTitle = convertView.findViewById(R.id.text_title);
- viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle);
- }
- convertView.setOnLongClickListener(this);
- } else {
- convertView = inflater.inflate(R.layout.section_item, parent, false);
- viewHolder.txtTitle = convertView.findViewById(R.id.text_title);
- }
- convertView.setTag(viewHolder);
- } else {
- viewHolder = (LogAdapter.ViewHolder) convertView.getTag();
- }
- if (type == ContentType.Item) {
- final LogItem data = (LogItem) getItem(position);
- viewHolder.txtTitle.setText(data.getMessage());
- if (!compact)
- viewHolder.txtSub.setText(data.getLevel());
- } else {
- viewHolder.txtTitle.setText((String) getItem(position));
- }
- viewHolder.position = position;
- return convertView;
- }
-
- private static class ViewHolder {
- TextView txtTitle;
- TextView txtSub;
- int position;
- }
-}
diff --git a/app/src/main/java/emu/skyline/MainActivity.java b/app/src/main/java/emu/skyline/MainActivity.java
deleted file mode 100644
index 551b6bef..00000000
--- a/app/src/main/java/emu/skyline/MainActivity.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package emu.skyline;
-
-import android.Manifest;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.Surface;
-import android.view.View;
-import android.widget.ListView;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.SearchView;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.preference.PreferenceManager;
-
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.android.material.snackbar.Snackbar;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-public class MainActivity extends AppCompatActivity implements View.OnClickListener {
-
- static {
- System.loadLibrary("skyline");
- }
-
- private SharedPreferences sharedPreferences;
- private GameAdapter adapter;
-
- private void notifyUser(final String text) {
- Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show();
- }
-
- private List findFile(final String ext, final File file, @Nullable List files) {
- if (files == null)
- files = new ArrayList<>();
- final File[] list = file.listFiles();
- if (list != null) {
- for (final File file_i : list) {
- if (file_i.isDirectory()) {
- files = findFile(ext, file_i, files);
- } else {
- try {
- final String file_str = file_i.getName();
- if (ext.equalsIgnoreCase(file_str.substring(file_str.lastIndexOf(".") + 1))) {
- if (NroLoader.verifyFile(file_i.getAbsolutePath())) {
- files.add(file_i);
- }
- }
- } catch (final StringIndexOutOfBoundsException e) {
- Log.w("findFile", Objects.requireNonNull(e.getMessage()));
- }
- }
- }
- }
- return files;
- }
-
- private void RefreshFiles(final boolean try_load) {
- if (try_load) {
- try {
- adapter.load(new File(getApplicationInfo().dataDir + "/roms.bin"));
- return;
- } catch (final Exception e) {
- Log.w("refreshFiles", "Ran into exception while loading: " + Objects.requireNonNull(e.getMessage()));
- }
- }
- adapter.clear();
- final List files = findFile("nro", new File(sharedPreferences.getString("search_location", "")), null);
- if (!files.isEmpty()) {
- adapter.add(getString(R.string.nro), ContentType.Header);
- for (final File file : files)
- adapter.add(new GameItem(file), ContentType.Item);
- } else {
- adapter.add(getString(R.string.no_rom), ContentType.Header);
- }
- try {
- adapter.save(new File(getApplicationInfo().dataDir + "/roms.bin"));
- } catch (final IOException e) {
- Log.w("refreshFiles", "Ran into exception while saving: " + Objects.requireNonNull(e.getMessage()));
- }
- }
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
- System.exit(0);
- }
- setContentView(R.layout.main_activity);
- PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
- sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- setSupportActionBar(findViewById(R.id.toolbar));
- final FloatingActionButton log_fab = findViewById(R.id.log_fab);
- log_fab.setOnClickListener(this);
- adapter = new GameAdapter(this);
- final ListView game_list = findViewById(R.id.game_list);
- game_list.setAdapter(adapter);
- game_list.setOnItemClickListener((parent, view, position, id) -> {
- if (adapter.getItemViewType(position) == ContentType.Item) {
- final GameItem item = ((GameItem) parent.getItemAtPosition(position));
- final Intent intent = new Intent(this, android.app.NativeActivity.class);
- intent.putExtra("rom", item.getPath());
- intent.putExtra("prefs", getApplicationInfo().dataDir + "/shared_prefs/" + getApplicationInfo().packageName + "_preferences.xml");
- intent.putExtra("log", getApplicationInfo().dataDir + "/skyline.log");
- startActivity(intent);
- }
- });
- RefreshFiles(true);
- }
-
- @Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- getMenuInflater().inflate(R.menu.toolbar_main, menu);
- final MenuItem mSearch = menu.findItem(R.id.action_search_main);
- SearchView searchView = (SearchView) mSearch.getActionView();
- searchView.setSubmitButtonEnabled(false);
- searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
- public boolean onQueryTextSubmit(final String query) {
- searchView.clearFocus();
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(final String newText) {
- adapter.getFilter().filter(newText);
- return true;
- }
- });
- return super.onCreateOptionsMenu(menu);
- }
-
- public void onClick(final View view) {
- if (view.getId() == R.id.log_fab)
- startActivity(new Intent(this, LogActivity.class));
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_settings:
- startActivity(new Intent(this, SettingsActivity.class));
- return true;
- case R.id.action_refresh:
- RefreshFiles(false);
- notifyUser(getString(R.string.refreshed));
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
-
- public native void loadFile(String rom_path, String preference_path, String log_path, Surface surface);
-}
diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt
new file mode 100644
index 00000000..fe664c38
--- /dev/null
+++ b/app/src/main/java/emu/skyline/MainActivity.kt
@@ -0,0 +1,165 @@
+package emu.skyline
+
+import android.content.Intent
+import android.content.SharedPreferences
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.AdapterView
+import android.widget.AdapterView.OnItemClickListener
+import android.widget.ListView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
+import androidx.documentfile.provider.DocumentFile
+import androidx.preference.PreferenceManager
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.google.android.material.snackbar.Snackbar
+import emu.skyline.adapter.GameAdapter
+import emu.skyline.adapter.GameItem
+import emu.skyline.loader.BaseLoader
+import emu.skyline.loader.NroLoader
+import emu.skyline.loader.TitleEntry
+import emu.skyline.utility.RandomAccessDocument
+import java.io.File
+import java.io.IOException
+import java.util.*
+
+class MainActivity : AppCompatActivity(), View.OnClickListener {
+ private lateinit var sharedPreferences: SharedPreferences
+ private var adapter = GameAdapter(this)
+ private fun notifyUser(text: String) {
+ Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show()
+ }
+
+ private fun findFile(ext: String, loader: BaseLoader, directory: DocumentFile, entries: MutableList): MutableList {
+ var mEntries = entries
+ for (file in directory.listFiles()) {
+ if (file.isDirectory) {
+ mEntries = findFile(ext, loader, file, mEntries)
+ } else {
+ try {
+ if (file.name != null) {
+ if (ext.equals(file.name?.substring((file.name!!.lastIndexOf(".")) + 1), ignoreCase = true)) {
+ val document = RandomAccessDocument(this, file)
+ if (loader.verifyFile(document))
+ mEntries.add(loader.getTitleEntry(document, file.uri))
+ document.close()
+ }
+ }
+ } catch (e: StringIndexOutOfBoundsException) {
+ Log.w("findFile", e.message!!)
+ }
+ }
+ }
+ return mEntries
+ }
+
+ private fun refreshFiles(tryLoad: Boolean) {
+ if (tryLoad) {
+ try {
+ adapter.load(File(applicationInfo.dataDir + "/roms.bin"))
+ return
+ } catch (e: Exception) {
+ Log.w("refreshFiles", "Ran into exception while loading: " + e.message)
+ }
+ }
+ adapter.clear()
+ val entries: List = findFile("nro", NroLoader(this), DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!, ArrayList())
+ if (entries.isNotEmpty()) {
+ adapter.addHeader(getString(R.string.nro))
+ for (entry in entries)
+ adapter.addItem(GameItem(entry))
+ } else {
+ adapter.addHeader(getString(R.string.no_rom))
+ }
+ try {
+ adapter.save(File(applicationInfo.dataDir + "/roms.bin"))
+ } catch (e: IOException) {
+ Log.w("refreshFiles", "Ran into exception while saving: " + e.message)
+ }
+ sharedPreferences.edit().putBoolean("refresh_required", false).apply()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.main_activity)
+ PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+ setSupportActionBar(findViewById(R.id.toolbar))
+ val logFab = findViewById(R.id.log_fab)
+ logFab.setOnClickListener(this)
+ val gameList = findViewById(R.id.game_list)
+ gameList.adapter = adapter
+ gameList.onItemClickListener = OnItemClickListener { parent: AdapterView<*>, _: View?, position: Int, _: Long ->
+ val item = parent.getItemAtPosition(position)
+ if (item is GameItem) {
+ val intent = Intent(this, GameActivity::class.java)
+ intent.putExtra("romUri", item.uri)
+ intent.putExtra("romType", item.meta.romType.ordinal)
+ startActivity(intent)
+ }
+ }
+ if (sharedPreferences.getString("search_location", "") == "") {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ this.startActivityForResult(intent, 1)
+ } else
+ refreshFiles(!sharedPreferences.getBoolean("refresh_required", false))
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.toolbar_main, menu)
+ val mSearch = menu.findItem(R.id.action_search_main)
+ val searchView = mSearch.actionView as SearchView
+ searchView.isSubmitButtonEnabled = false
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ searchView.clearFocus()
+ return false
+ }
+
+ override fun onQueryTextChange(newText: String): Boolean {
+ adapter.filter.filter(newText)
+ return true
+ }
+ })
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onClick(view: View) {
+ if (view.id == R.id.log_fab) startActivity(Intent(this, LogActivity::class.java))
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.action_settings -> {
+ startActivity(Intent(this, SettingsActivity::class.java))
+ true
+ }
+ R.id.action_refresh -> {
+ refreshFiles(false)
+ notifyUser(getString(R.string.refreshed))
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if(sharedPreferences.getBoolean("refresh_required", false))
+ refreshFiles(false)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (resultCode == RESULT_OK) {
+ if (requestCode == 1) {
+ sharedPreferences.edit().putString("search_location", data!!.data.toString()).apply()
+ refreshFiles(!sharedPreferences.getBoolean("refresh_required", false))
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/emu/skyline/NroLoader.java b/app/src/main/java/emu/skyline/NroLoader.java
deleted file mode 100644
index 083e8464..00000000
--- a/app/src/main/java/emu/skyline/NroLoader.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package emu.skyline;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.util.Log;
-
-import java.io.IOException;
-import java.io.RandomAccessFile;
-
-final class TitleEntry {
- private final String name;
- private final String author;
- private final Bitmap icon;
-
- TitleEntry(final String name, final String author, final Bitmap icon) {
- this.name = name;
- this.author = author;
- this.icon = icon;
- }
-
- public String getName() {
- return name;
- }
-
- public String getAuthor() {
- return author;
- }
-
- public Bitmap getIcon() {
- return icon;
- }
-}
-
-class NroLoader {
- static TitleEntry getTitleEntry(final String file) {
- try {
- final RandomAccessFile f = new RandomAccessFile(file, "r");
- f.seek(0x18); // Skip to NroHeader.size
- final int asetOffset = Integer.reverseBytes(f.readInt());
- f.seek(asetOffset); // Skip to the offset specified by NroHeader.size
- final byte[] buffer = new byte[4];
- f.read(buffer);
- if (!(new String(buffer).equals("ASET")))
- throw new IOException();
-
- f.skipBytes(0x4);
- final long iconOffset = Long.reverseBytes(f.readLong());
- final int iconSize = Integer.reverseBytes(f.readInt());
- if (iconOffset == 0 || iconSize == 0)
- throw new IOException();
- f.seek(asetOffset + iconOffset);
- final byte[] iconData = new byte[iconSize];
- f.read(iconData);
- final Bitmap icon = BitmapFactory.decodeByteArray(iconData, 0, iconSize);
-
- f.seek(asetOffset + 0x18);
- final long nacpOffset = Long.reverseBytes(f.readLong());
- final long nacpSize = Long.reverseBytes(f.readLong());
- if (nacpOffset == 0 || nacpSize == 0)
- throw new IOException();
- f.seek(asetOffset + nacpOffset);
- final byte[] name = new byte[0x200];
- f.read(name);
- final byte[] author = new byte[0x100];
- f.read(author);
-
- return new TitleEntry(new String(name).trim(), new String(author).trim(), icon);
- } catch (final IOException e) {
- Log.e("app_process64", "Error while loading ASET: " + e.getMessage());
- return null;
- }
- }
-
- static boolean verifyFile(final String file) {
- try {
- final RandomAccessFile f = new RandomAccessFile(file, "r");
- f.seek(0x10); // Skip to NroHeader.magic
- final byte[] buffer = new byte[4];
- f.read(buffer);
- if (!(new String(buffer).equals("NRO0")))
- return false;
- } catch (final IOException e) {
- return false;
- }
- return true;
- }
-}
diff --git a/app/src/main/java/emu/skyline/SettingsActivity.java b/app/src/main/java/emu/skyline/SettingsActivity.java
deleted file mode 100644
index e5de5cde..00000000
--- a/app/src/main/java/emu/skyline/SettingsActivity.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package emu.skyline;
-
-import android.os.Bundle;
-
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.preference.PreferenceFragmentCompat;
-
-public class SettingsActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.settings_activity);
- getSupportFragmentManager()
- .beginTransaction()
- .replace(R.id.settings, new SettingsActivity.HeaderFragment())
- .commit();
- setSupportActionBar(findViewById(R.id.toolbar));
- final ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(true);
- }
- }
-
- public static class HeaderFragment extends PreferenceFragmentCompat {
- @Override
- public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
- setPreferencesFromResource(R.xml.preferences, rootKey);
- }
- }
-}
diff --git a/app/src/main/java/emu/skyline/SettingsActivity.kt b/app/src/main/java/emu/skyline/SettingsActivity.kt
new file mode 100644
index 00000000..5eedc119
--- /dev/null
+++ b/app/src/main/java/emu/skyline/SettingsActivity.kt
@@ -0,0 +1,38 @@
+package emu.skyline
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.preference.PreferenceFragmentCompat
+import kotlinx.android.synthetic.main.log_activity.*
+
+class SettingsActivity : AppCompatActivity() {
+ private val preferenceFragment: PreferenceFragment = PreferenceFragment()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.settings_activity)
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.settings, preferenceFragment)
+ .commit()
+ setSupportActionBar(toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ preferenceFragment.refreshPreferences()
+ }
+
+ class PreferenceFragment : PreferenceFragmentCompat() {
+ fun refreshPreferences() {
+ preferenceScreen = null
+ addPreferencesFromResource(R.xml.preferences)
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.preferences, rootKey)
+ }
+ }
+}
diff --git a/app/src/main/java/emu/skyline/adapter/GameAdapter.kt b/app/src/main/java/emu/skyline/adapter/GameAdapter.kt
new file mode 100644
index 00000000..be31a38e
--- /dev/null
+++ b/app/src/main/java/emu/skyline/adapter/GameAdapter.kt
@@ -0,0 +1,101 @@
+package emu.skyline.adapter
+
+import android.app.Dialog
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import android.widget.TextView
+import emu.skyline.R
+import emu.skyline.loader.TitleEntry
+
+internal class GameItem(val meta: TitleEntry) : BaseItem() {
+ val icon: Bitmap?
+ get() = meta.icon
+
+ val title: String
+ get() = meta.name + " (" + type + ")"
+
+ val subTitle: String?
+ get() = meta.author
+
+ val uri: Uri
+ get() = meta.uri
+
+ private val type: String
+ get() = meta.romType.name
+
+ override fun key(): String? {
+ return if (meta.valid) meta.name + " " + meta.author else meta.name
+ }
+}
+
+internal class GameAdapter(val context: Context?) : HeaderAdapter(), View.OnClickListener {
+ fun addHeader(string: String) {
+ super.addHeader(BaseHeader(string))
+ }
+
+ override fun onClick(view: View) {
+ val position = view.tag as Int
+ if (getItem(position) is GameItem) {
+ val item = getItem(position) as GameItem
+ if (view.id == R.id.icon) {
+ val builder = Dialog(context!!)
+ builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ val imageView = ImageView(context)
+ imageView.setImageBitmap(item.icon)
+ builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
+ builder.show()
+ }
+ }
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ var view = convertView
+ val viewHolder: ViewHolder
+ val item = elementArray[visibleArray[position]]
+ if (view == null) {
+ viewHolder = ViewHolder()
+ if (item is GameItem) {
+ val inflater = LayoutInflater.from(context)
+ view = inflater.inflate(R.layout.game_item, parent, false)
+ viewHolder.icon = view.findViewById(R.id.icon)
+ viewHolder.txtTitle = view.findViewById(R.id.text_title)
+ viewHolder.txtSub = view.findViewById(R.id.text_subtitle)
+ view.tag = viewHolder
+ } else if (item is BaseHeader) {
+ val inflater = LayoutInflater.from(context)
+ view = inflater.inflate(R.layout.section_item, parent, false)
+ viewHolder.txtTitle = view.findViewById(R.id.text_title)
+ view.tag = viewHolder
+ }
+ } else {
+ viewHolder = view.tag as ViewHolder
+ }
+ if (item is GameItem) {
+ val data = getItem(position) as GameItem
+ viewHolder.txtTitle!!.text = data.title
+ viewHolder.txtSub!!.text = data.subTitle
+ viewHolder.icon!!.setImageBitmap(data.icon)
+ viewHolder.icon!!.setOnClickListener(this)
+ viewHolder.icon!!.tag = position
+ } else {
+ viewHolder.txtTitle!!.text = (getItem(position) as BaseHeader).title
+ }
+ return view!!
+ }
+
+ private class ViewHolder {
+ var icon: ImageView? = null
+ var txtTitle: TextView? = null
+ var txtSub: TextView? = null
+ }
+}
diff --git a/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt b/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt
new file mode 100644
index 00000000..0cf1f204
--- /dev/null
+++ b/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt
@@ -0,0 +1,148 @@
+package emu.skyline.adapter
+
+import android.util.SparseIntArray
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.Filter
+import android.widget.Filterable
+import me.xdrop.fuzzywuzzy.FuzzySearch
+import java.io.*
+import java.util.*
+import kotlin.collections.ArrayList
+
+enum class ElementType(val type: Int) {
+ Header(0x0),
+ Item(0x1)
+}
+
+/**
+ * @brief This is the interface class that all element classes inherit from
+ */
+abstract class BaseElement constructor(val elementType: ElementType) : Serializable
+
+/**
+ * @brief This is the interface class that all header classes inherit from
+ */
+class BaseHeader constructor(val title: String) : BaseElement(ElementType.Header)
+
+/**
+ * @brief This is the interface class that all item classes inherit from
+ */
+abstract class BaseItem : BaseElement(ElementType.Item) {
+ abstract fun key(): String?
+}
+
+internal abstract class HeaderAdapter : BaseAdapter(), Filterable, Serializable {
+ var elementArray: ArrayList = ArrayList()
+ var visibleArray: ArrayList = ArrayList()
+ private var searchTerm = ""
+
+ fun addItem(element: ItemType) {
+ elementArray.add(element)
+ if (searchTerm.isNotEmpty())
+ filter.filter(searchTerm)
+ else {
+ visibleArray.add(elementArray.size - 1)
+ notifyDataSetChanged()
+ }
+ }
+
+ fun addHeader(element: HeaderType) {
+ elementArray.add(element)
+ if (searchTerm.isNotEmpty())
+ filter.filter(searchTerm)
+ else {
+ visibleArray.add(elementArray.size - 1)
+ notifyDataSetChanged()
+ }
+ }
+
+ internal inner class State(val elementArray: ArrayList) : Serializable
+
+ @Throws(IOException::class)
+ fun save(file: File) {
+ val fileObj = FileOutputStream(file)
+ val out = ObjectOutputStream(fileObj)
+ out.writeObject(elementArray)
+ out.close()
+ fileObj.close()
+ }
+
+ @Throws(IOException::class, ClassNotFoundException::class)
+ open fun load(file: File) {
+ val fileObj = FileInputStream(file)
+ val input = ObjectInputStream(fileObj)
+ elementArray = input.readObject() as ArrayList
+ input.close()
+ fileObj.close()
+ filter.filter(searchTerm)
+ }
+
+ fun clear() {
+ elementArray.clear()
+ visibleArray.clear()
+ notifyDataSetChanged()
+ }
+
+ override fun getCount(): Int {
+ return visibleArray.size
+ }
+
+ override fun getItem(index: Int): BaseElement? {
+ return elementArray[visibleArray[index]]
+ }
+
+ override fun getItemId(position: Int): Long {
+ return position.toLong()
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return elementArray[visibleArray[position]]!!.elementType.type
+ }
+
+ override fun getViewTypeCount(): Int {
+ return ElementType.values().size
+ }
+
+ abstract override fun getView(position: Int, convertView: View?, parent: ViewGroup): View
+
+ override fun getFilter(): Filter {
+ return object : Filter() {
+ override fun performFiltering(charSequence: CharSequence): FilterResults {
+ val results = FilterResults()
+ searchTerm = (charSequence as String).toLowerCase(Locale.getDefault()).replace(" ".toRegex(), "")
+ if (charSequence.isEmpty()) {
+ results.values = elementArray.indices.toMutableList()
+ results.count = elementArray.size
+ } else {
+ val filterData = ArrayList()
+ val keyArray = ArrayList()
+ val keyIndex = SparseIntArray()
+ for (index in elementArray.indices) {
+ val item = elementArray[index]!!
+ if (item is BaseItem) {
+ keyIndex.append(keyArray.size, index)
+ keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
+ }
+ }
+ val topResults = FuzzySearch.extractTop(searchTerm, keyArray, searchTerm.length)
+ val avgScore: Int = topResults.sumBy { it.score } / topResults.size
+ for (result in topResults)
+ if (result.score > avgScore)
+ filterData.add(keyIndex[result.index])
+ results.values = filterData
+ results.count = filterData.size
+ }
+ return results
+ }
+
+ override fun publishResults(charSequence: CharSequence, results: FilterResults) {
+ if (results.values is ArrayList<*>) {
+ visibleArray = results.values as ArrayList
+ notifyDataSetChanged()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/emu/skyline/adapter/LogAdapter.kt b/app/src/main/java/emu/skyline/adapter/LogAdapter.kt
new file mode 100644
index 00000000..32791cde
--- /dev/null
+++ b/app/src/main/java/emu/skyline/adapter/LogAdapter.kt
@@ -0,0 +1,85 @@
+package emu.skyline.adapter
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnLongClickListener
+import android.view.ViewGroup
+import android.widget.TextView
+import android.widget.Toast
+import emu.skyline.R
+
+internal class LogItem(val message: String, val level: String) : BaseItem() {
+ override fun key(): String? {
+ return message
+ }
+}
+
+internal class LogAdapter internal constructor(val context: Context, val compact: Boolean, private val debug_level: Int, private val level_str: Array) : HeaderAdapter(), OnLongClickListener {
+ private val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+ fun add(logLine: String) {
+ try {
+ val logMeta = logLine.split("|", limit = 3)
+ if (logMeta[0].startsWith("1")) {
+ val level = logMeta[1].toInt()
+ if (level > debug_level) return
+ addItem(LogItem(logMeta[2].replace('\\', '\n'), level_str[level]))
+ } else {
+ addHeader(BaseHeader(logMeta[1]))
+ }
+ } catch (ignored: IndexOutOfBoundsException) {
+ }
+ }
+
+ override fun onLongClick(view: View): Boolean {
+ val item = getItem((view.tag as ViewHolder).position) as LogItem
+ clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
+ Toast.makeText(view.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
+ return false
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ var view = convertView
+ val viewHolder: ViewHolder
+ val item = elementArray[visibleArray[position]]
+ if (view == null) {
+ viewHolder = ViewHolder()
+ val inflater = LayoutInflater.from(context)
+ if (item is LogItem) {
+ if (compact) {
+ view = inflater.inflate(R.layout.log_item_compact, parent, false)
+ viewHolder.txtTitle = view.findViewById(R.id.text_title)
+ } else {
+ view = inflater.inflate(R.layout.log_item, parent, false)
+ viewHolder.txtTitle = view.findViewById(R.id.text_title)
+ viewHolder.txtSub = view.findViewById(R.id.text_subtitle)
+ }
+ view.setOnLongClickListener(this)
+ } else if (item is BaseHeader) {
+ view = inflater.inflate(R.layout.section_item, parent, false)
+ viewHolder.txtTitle = view.findViewById(R.id.text_title)
+ }
+ view!!.tag = viewHolder
+ } else {
+ viewHolder = view.tag as ViewHolder
+ }
+ if (item is LogItem) {
+ viewHolder.txtTitle!!.text = item.message
+ if (!compact) viewHolder.txtSub!!.text = item.level
+ } else if (item is BaseHeader) {
+ viewHolder.txtTitle!!.text = item.title
+ }
+ viewHolder.position = position
+ return view!!
+ }
+
+ private class ViewHolder {
+ var txtTitle: TextView? = null
+ var txtSub: TextView? = null
+ var position = 0
+ }
+
+}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index e86fe0a9..00000000
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index e2561079..00000000
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index af9cd3af..cdbc8ed5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8,12 +8,10 @@
Refresh
The list of ROMs has been refreshed.
- Launching
ASET Header Missing
Icon
Cannot find any ROMs
NROs
- NSOs
Clear
Share
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 01fd475c..17a1fcd7 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -7,12 +7,4 @@
- @color/colorAccent
-
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 4c92e715..d30d57a2 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -52,15 +52,4 @@
app:key="operation_mode"
app:title="@string/use_docked" />
-
-
-
diff --git a/build.gradle b/build.gradle
index 753b4d73..5d423337 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
+ ext.kotlin_version = '1.3.60'
repositories {
google()
jcenter()
@@ -8,6 +9,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files