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:
◱ PixelyIon 2019-12-02 19:11:23 +05:30 committed by ◱ PixelyIon
parent 9db0c20c92
commit e4dc602f4d
9 changed files with 334 additions and 31 deletions

View File

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

View File

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

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

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

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

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

View File

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

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

View File

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