mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-28 23:05:29 +03: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.
|
# Skyline Proguard Rules
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
-keep class emu.skyline.loader.TitleEntry {
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
void writeObject(java.io.ObjectOutputStream);
|
||||||
# class:
|
void readObject(java.io.ObjectInputStream);
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
}
|
||||||
# public *;
|
-keepclassmembernames class emu.skyline.GameActivity { *; }
|
||||||
#}
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="emu.skyline">
|
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.READ_INTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
@ -13,13 +12,19 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:extractNativeLibs="false"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
android:icon="@drawable/logo_skyline"
|
android:icon="@drawable/logo_skyline"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
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
|
<activity
|
||||||
android:name="emu.skyline.LogActivity"
|
android:name="emu.skyline.LogActivity"
|
||||||
android:label="@string/log"
|
android:label="@string/log"
|
||||||
@ -36,10 +41,14 @@
|
|||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="emu.skyline.MainActivity" />
|
android:value="emu.skyline.MainActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name="emu.skyline.utility.FolderActivity">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="emu.skyline.SettingsActivity" />
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="android.app.NativeActivity"
|
android:name="emu.skyline.GameActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
android:parentActivityName="emu.skyline.MainActivity"
|
|
||||||
android:screenOrientation="landscape">
|
android:screenOrientation="landscape">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.lib_name"
|
android:name="android.app.lib_name"
|
||||||
@ -48,12 +57,6 @@
|
|||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="emu.skyline.MainActivity" />
|
android:value="emu.skyline.MainActivity" />
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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
|
<PreferenceCategory
|
||||||
android:key="category_search"
|
android:key="category_search"
|
||||||
android:title="@string/search">
|
android:title="@string/search">
|
||||||
<EditTextPreference
|
<emu.skyline.utility.FolderPreference
|
||||||
android:defaultValue="/sdcard/"
|
|
||||||
app:key="search_location"
|
app:key="search_location"
|
||||||
app:title="@string/search_location"
|
app:title="@string/search_location"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
Loading…
Reference in New Issue
Block a user