Move from Java to Kotlin

This commit converts all Java code to Kotlin and improves the structure and performance of most rewritten parts.
This commit is contained in:
◱ PixelyIon 2019-12-02 19:09:08 +05:30 committed by ◱ PixelyIon
parent 38716989ae
commit 9db0c20c92
22 changed files with 781 additions and 1140 deletions

View File

@ -114,7 +114,6 @@
<option name="ignoreIntegerCharCasts" value="false" />
<option name="ignoreOverflowingByteCasts" value="false" />
</inspection_tool>
<inspection_tool class="CastToConcreteClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CastToIncompatibleInterface" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ChainedMethodCall" enabled="true" level="WARNING" enabled_by_default="true">

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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<GameItem> 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;
}
}

View File

@ -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<ItemType extends BaseItem> extends BaseAdapter implements Filterable, Serializable {
@SuppressLint("StaticFieldLeak")
static Context mContext;
ArrayList<ContentType> type_array;
ArrayList<ItemType> item_array;
private ArrayList<ContentType> type_array_uf;
private ArrayList<String> 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<ContentType> filter_data = new ArrayList<>();
final ArrayList<String> 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<ContentType>) filterResults.values;
notifyDataSetChanged();
}
};
}
class State<StateType> implements Serializable {
private final ArrayList<StateType> item_array;
private final ArrayList<String> header_array;
private final ArrayList<ContentType> type_array;
State(final ArrayList<StateType> item_array, final ArrayList<String> header_array, final ArrayList<ContentType> type_array) {
this.item_array = item_array;
this.header_array = header_array;
this.type_array = type_array;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<ListView>(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)
}
}
}

View File

@ -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<LogItem> 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;
}
}

View File

@ -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<File> findFile(final String ext, final File file, @Nullable List<File> 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<File> 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);
}

View File

@ -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<TitleEntry>): MutableList<TitleEntry> {
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<TitleEntry> = 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<FloatingActionButton>(R.id.log_fab)
logFab.setOnClickListener(this)
val gameList = findViewById<ListView>(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))
}
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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)
}
}
}

View File

@ -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<GameItem, BaseHeader>(), 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
}
}

View File

@ -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<ItemType : BaseItem?, HeaderType : BaseHeader?> : BaseAdapter(), Filterable, Serializable {
var elementArray: ArrayList<BaseElement?> = ArrayList()
var visibleArray: ArrayList<Int> = 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<BaseElement?>) : 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<BaseElement?>
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<Int>()
val keyArray = ArrayList<String>()
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<Int>
notifyDataSetChanged()
}
}
}
}
}

View File

@ -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<String>) : HeaderAdapter<LogItem, BaseHeader>(), 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
}
}

View File

@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -8,12 +8,10 @@
<string name="refresh">Refresh</string>
<!-- Main -->
<string name="refreshed">The list of ROMs has been refreshed.</string>
<string name="launching">Launching</string>
<string name="aset_missing">ASET Header Missing</string>
<string name="icon">Icon</string>
<string name="no_rom">Cannot find any ROMs</string>
<string name="nro">NROs</string>
<string name="nso">NSOs</string>
<!-- Toolbar Logger -->
<string name="clear">Clear</string>
<string name="share">Share</string>

View File

@ -7,12 +7,4 @@
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="ToolbarTheme">
<!-- Query Text Color -->
<item name="android:textColorPrimary">@android:color/white</item>
<!-- Hint Color -->
<item name="android:textColorHint">@android:color/darker_gray</item>
<!-- Icon Color -->
<item name="android:tint">@android:color/white</item>
</style>
</resources>

View File

@ -52,15 +52,4 @@
app:key="operation_mode"
app:title="@string/use_docked" />
</PreferenceCategory>
<PreferenceCategory
android:key="category_localization"
android:title="@string/localization">
<ListPreference
android:defaultValue="sys"
android:entries="@array/language_names"
android:entryValues="@array/language_values"
app:key="localization_language"
app:title="@string/localization_language"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -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