mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-11-29 20:04:17 +01:00
Migrate to SAF APIs for file access
This commit moves from conventional file access to using URIs and such provided by the SAF APIs.
This commit is contained in:
parent
9db0c20c92
commit
e4dc602f4d
24
app/proguard-rules.pro
vendored
24
app/proguard-rules.pro
vendored
@ -1,21 +1,9 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# Skyline Proguard Rules
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keep class emu.skyline.loader.TitleEntry {
|
||||
void writeObject(java.io.ObjectOutputStream);
|
||||
void readObject(java.io.ObjectInputStream);
|
||||
}
|
||||
-keepclassmembernames class emu.skyline.GameActivity { *; }
|
||||
|
@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="emu.skyline">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
@ -13,13 +12,19 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:extractNativeLibs="false"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:icon="@drawable/logo_skyline"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||
<activity android:name="emu.skyline.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="emu.skyline.LogActivity"
|
||||
android:label="@string/log"
|
||||
@ -36,10 +41,14 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.MainActivity" />
|
||||
</activity>
|
||||
<activity android:name="emu.skyline.utility.FolderActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.SettingsActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="android.app.NativeActivity"
|
||||
android:name="emu.skyline.GameActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:parentActivityName="emu.skyline.MainActivity"
|
||||
android:screenOrientation="landscape">
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
@ -48,12 +57,6 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.MainActivity" />
|
||||
</activity>
|
||||
<activity android:name="emu.skyline.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
54
app/src/main/java/emu/skyline/loader/BaseLoader.kt
Normal file
54
app/src/main/java/emu/skyline/loader/BaseLoader.kt
Normal file
@ -0,0 +1,54 @@
|
||||
package emu.skyline.loader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import emu.skyline.R
|
||||
import emu.skyline.utility.RandomAccessDocument
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
|
||||
enum class TitleFormat {
|
||||
NRO, XCI, NSP
|
||||
}
|
||||
|
||||
internal class TitleEntry(var name: String, var author: String, var romType: TitleFormat, var valid: Boolean, @Transient var uri: Uri, @Transient var icon: Bitmap) : Serializable {
|
||||
constructor(context: Context, author: String, romType: TitleFormat, valid: Boolean, uri: Uri) : this("", author, romType, valid, uri, context.resources.getDrawable(R.drawable.ic_missing_icon, context.theme).toBitmap(256, 256)) {
|
||||
context.contentResolver.query(uri, null, null, null, null)!!.use { cursor ->
|
||||
val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
name = cursor.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeObject(output: ObjectOutputStream) {
|
||||
output.writeUTF(name)
|
||||
output.writeUTF(author)
|
||||
output.writeObject(romType)
|
||||
output.writeUTF(uri.toString())
|
||||
output.writeBoolean(valid)
|
||||
icon.compress(Bitmap.CompressFormat.WEBP, 100, output)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ClassNotFoundException::class)
|
||||
private fun readObject(input: ObjectInputStream) {
|
||||
name = input.readUTF()
|
||||
author = input.readUTF()
|
||||
romType = input.readObject() as TitleFormat
|
||||
uri = Uri.parse(input.readUTF())
|
||||
valid = input.readBoolean()
|
||||
icon = BitmapFactory.decodeStream(input)
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class BaseLoader(val context: Context, val romType: TitleFormat) {
|
||||
abstract fun getTitleEntry(file: RandomAccessDocument, uri: Uri): TitleEntry
|
||||
|
||||
abstract fun verifyFile(file: RandomAccessDocument): Boolean
|
||||
}
|
53
app/src/main/java/emu/skyline/loader/NroLoader.kt
Normal file
53
app/src/main/java/emu/skyline/loader/NroLoader.kt
Normal file
@ -0,0 +1,53 @@
|
||||
package emu.skyline.loader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import emu.skyline.R
|
||||
import emu.skyline.utility.RandomAccessDocument
|
||||
import java.io.IOException
|
||||
|
||||
internal class NroLoader(context: Context) : BaseLoader(context, TitleFormat.NRO) {
|
||||
override fun getTitleEntry(file: RandomAccessDocument, uri: Uri): TitleEntry {
|
||||
return try {
|
||||
file.seek(0x18) // Skip to NroHeader.size
|
||||
val asetOffset = Integer.reverseBytes(file.readInt())
|
||||
file.seek(asetOffset.toLong()) // Skip to the offset specified by NroHeader.size
|
||||
val buffer = ByteArray(4)
|
||||
file.read(buffer)
|
||||
if (String(buffer) != "ASET") throw IOException()
|
||||
file.skipBytes(0x4)
|
||||
val iconOffset = java.lang.Long.reverseBytes(file.readLong())
|
||||
val iconSize = Integer.reverseBytes(file.readInt())
|
||||
if (iconOffset == 0L || iconSize == 0) throw IOException()
|
||||
file.seek(asetOffset + iconOffset)
|
||||
val iconData = ByteArray(iconSize)
|
||||
file.read(iconData)
|
||||
val icon = BitmapFactory.decodeByteArray(iconData, 0, iconSize)
|
||||
file.seek(asetOffset + 0x18.toLong())
|
||||
val nacpOffset = java.lang.Long.reverseBytes(file.readLong())
|
||||
val nacpSize = java.lang.Long.reverseBytes(file.readLong())
|
||||
if (nacpOffset == 0L || nacpSize == 0L) throw IOException()
|
||||
file.seek(asetOffset + nacpOffset)
|
||||
val name = ByteArray(0x200)
|
||||
file.read(name)
|
||||
val author = ByteArray(0x100)
|
||||
file.read(author)
|
||||
TitleEntry(String(name).substringBefore((0.toChar())), String(author).substringBefore((0.toChar())), romType, true, uri, icon)
|
||||
} catch (e: IOException) {
|
||||
TitleEntry(context, context.getString(R.string.aset_missing), romType, false, uri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun verifyFile(file: RandomAccessDocument): Boolean {
|
||||
try {
|
||||
file.seek(0x10) // Skip to NroHeader.magic
|
||||
val buffer = ByteArray(4)
|
||||
file.read(buffer)
|
||||
if (String(buffer) != "NRO0") return false
|
||||
} catch (e: IOException) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
29
app/src/main/java/emu/skyline/utility/FolderActivity.kt
Normal file
29
app/src/main/java/emu/skyline/utility/FolderActivity.kt
Normal file
@ -0,0 +1,29 @@
|
||||
package emu.skyline.utility
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
class FolderActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
this.startActivityForResult(intent, 1)
|
||||
}
|
||||
|
||||
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == 1) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString("search_location", data!!.data.toString())
|
||||
.putBoolean("refresh_required", true)
|
||||
.apply()
|
||||
finish()
|
||||
}
|
||||
} else
|
||||
finish()
|
||||
}
|
||||
}
|
110
app/src/main/java/emu/skyline/utility/FolderPreference.kt
Normal file
110
app/src/main/java/emu/skyline/utility/FolderPreference.kt
Normal file
@ -0,0 +1,110 @@
|
||||
package emu.skyline.utility
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.TypedArray
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
|
||||
class FolderPreference : Preference {
|
||||
private var mDirectory: String? = null
|
||||
|
||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
summaryProvider = SimpleSummaryProvider.instance
|
||||
}
|
||||
|
||||
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
|
||||
summaryProvider = SimpleSummaryProvider.instance
|
||||
}
|
||||
|
||||
constructor(context: Context?) : super(context) {
|
||||
summaryProvider = SimpleSummaryProvider.instance
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
val intent = Intent(context, FolderActivity::class.java)
|
||||
(context as Activity).startActivityForResult(intent, 0)
|
||||
}
|
||||
|
||||
var directory: String?
|
||||
get() = mDirectory
|
||||
set(directory) {
|
||||
val changed = !TextUtils.equals(mDirectory, directory)
|
||||
if (changed) {
|
||||
mDirectory = directory
|
||||
persistString(directory)
|
||||
if (changed) {
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetDefaultValue(a: TypedArray, index: Int): Any {
|
||||
return a.getString(index)!!
|
||||
}
|
||||
|
||||
override fun onSetInitialValue(defaultValue: Any?) {
|
||||
directory = getPersistedString(defaultValue as String?)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
if (isPersistent) {
|
||||
return superState
|
||||
}
|
||||
val myState = SavedState(superState)
|
||||
myState.mDirectory = directory
|
||||
return myState
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
if (state == null || state.javaClass != SavedState::class.java) {
|
||||
super.onRestoreInstanceState(state)
|
||||
return
|
||||
}
|
||||
val myState = state as SavedState
|
||||
super.onRestoreInstanceState(myState.superState)
|
||||
directory = myState.mDirectory
|
||||
}
|
||||
|
||||
internal class SavedState : BaseSavedState {
|
||||
var mDirectory: String? = null
|
||||
|
||||
constructor(source: Parcel) : super(source) {
|
||||
mDirectory = source.readString()
|
||||
}
|
||||
|
||||
constructor(superState: Parcelable?) : super(superState)
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
super.writeToParcel(dest, flags)
|
||||
dest.writeString(mDirectory)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleSummaryProvider private constructor() : SummaryProvider<FolderPreference> {
|
||||
override fun provideSummary(preference: FolderPreference): CharSequence {
|
||||
return Uri.decode(preference.directory!!)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var sSimpleSummaryProvider: SimpleSummaryProvider? = null
|
||||
val instance: SimpleSummaryProvider?
|
||||
get() {
|
||||
if (sSimpleSummaryProvider == null) {
|
||||
sSimpleSummaryProvider = SimpleSummaryProvider()
|
||||
}
|
||||
return sSimpleSummaryProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package emu.skyline.utility
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class RandomAccessDocument(private var parcelFileDescriptor: ParcelFileDescriptor) {
|
||||
constructor(context: Context, file: DocumentFile) : this(context.contentResolver.openFileDescriptor(file.uri, "r")!!)
|
||||
|
||||
private val fileDescriptor = parcelFileDescriptor.fileDescriptor
|
||||
private var position: Long = 0
|
||||
|
||||
fun read(array: ByteArray): Int {
|
||||
val bytesRead = android.system.Os.pread(fileDescriptor, array, 0, array.size, position)
|
||||
position += bytesRead
|
||||
return bytesRead
|
||||
}
|
||||
|
||||
fun read(buffer: ByteBuffer): Int {
|
||||
val bytesRead = android.system.Os.pread(fileDescriptor, buffer.array(), 0, buffer.array().size, position)
|
||||
position += bytesRead
|
||||
return bytesRead
|
||||
}
|
||||
|
||||
fun readLong(): Long {
|
||||
val buffer: ByteBuffer = ByteBuffer.allocate(Long.SIZE_BYTES)
|
||||
read(buffer)
|
||||
return buffer.long
|
||||
}
|
||||
|
||||
fun readInt(): Int {
|
||||
val buffer: ByteBuffer = ByteBuffer.allocate(Int.SIZE_BYTES)
|
||||
read(buffer)
|
||||
return buffer.int
|
||||
}
|
||||
|
||||
fun seek(position: Long) {
|
||||
this.position = position
|
||||
}
|
||||
|
||||
fun skipBytes(position: Long) {
|
||||
this.position += position
|
||||
}
|
||||
|
||||
fun close() {
|
||||
parcelFileDescriptor.close()
|
||||
}
|
||||
}
|
18
app/src/main/res/layout/game_activity.xml
Normal file
18
app/src/main/res/layout/game_activity.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".GameActivity">
|
||||
|
||||
<SurfaceView
|
||||
android:id="@+id/game_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -19,8 +19,7 @@
|
||||
<PreferenceCategory
|
||||
android:key="category_search"
|
||||
android:title="@string/search">
|
||||
<EditTextPreference
|
||||
android:defaultValue="/sdcard/"
|
||||
<emu.skyline.utility.FolderPreference
|
||||
app:key="search_location"
|
||||
app:title="@string/search_location"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
Loading…
Reference in New Issue
Block a user