mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-11-22 20:09:17 +01:00
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:
parent
38716989ae
commit
9db0c20c92
@ -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">
|
||||
|
@ -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()
|
||||
}
|
||||
|
88
app/src/main/java/emu/skyline/GameActivity.kt
Normal file
88
app/src/main/java/emu/skyline/GameActivity.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
144
app/src/main/java/emu/skyline/LogActivity.kt
Normal file
144
app/src/main/java/emu/skyline/LogActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
165
app/src/main/java/emu/skyline/MainActivity.kt
Normal file
165
app/src/main/java/emu/skyline/MainActivity.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
38
app/src/main/java/emu/skyline/SettingsActivity.kt
Normal file
38
app/src/main/java/emu/skyline/SettingsActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
101
app/src/main/java/emu/skyline/adapter/GameAdapter.kt
Normal file
101
app/src/main/java/emu/skyline/adapter/GameAdapter.kt
Normal 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
|
||||
}
|
||||
}
|
148
app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt
Normal file
148
app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
85
app/src/main/java/emu/skyline/adapter/LogAdapter.kt
Normal file
85
app/src/main/java/emu/skyline/adapter/LogAdapter.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user