From e4dc602f4d2d38686581365602262370f71d156b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=97=B1=20PixelyIon?= Date: Mon, 2 Dec 2019 19:11:23 +0530 Subject: [PATCH] Migrate to SAF APIs for file access This commit moves from conventional file access to using URIs and such provided by the SAF APIs. --- app/proguard-rules.pro | 24 +--- app/src/main/AndroidManifest.xml | 25 ++-- .../java/emu/skyline/loader/BaseLoader.kt | 54 +++++++++ .../main/java/emu/skyline/loader/NroLoader.kt | 53 +++++++++ .../emu/skyline/utility/FolderActivity.kt | 29 +++++ .../emu/skyline/utility/FolderPreference.kt | 110 ++++++++++++++++++ .../skyline/utility/RandomAccessDocument.kt | 49 ++++++++ app/src/main/res/layout/game_activity.xml | 18 +++ app/src/main/res/xml/preferences.xml | 3 +- 9 files changed, 334 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/emu/skyline/loader/BaseLoader.kt create mode 100644 app/src/main/java/emu/skyline/loader/NroLoader.kt create mode 100644 app/src/main/java/emu/skyline/utility/FolderActivity.kt create mode 100644 app/src/main/java/emu/skyline/utility/FolderPreference.kt create mode 100644 app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt create mode 100644 app/src/main/res/layout/game_activity.xml diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b42451..ec37c031 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b4346405..431f1483 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" package="emu.skyline"> - @@ -13,13 +12,19 @@ + tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> + + + + + + + + + - - - - - - diff --git a/app/src/main/java/emu/skyline/loader/BaseLoader.kt b/app/src/main/java/emu/skyline/loader/BaseLoader.kt new file mode 100644 index 00000000..890888e8 --- /dev/null +++ b/app/src/main/java/emu/skyline/loader/BaseLoader.kt @@ -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 +} diff --git a/app/src/main/java/emu/skyline/loader/NroLoader.kt b/app/src/main/java/emu/skyline/loader/NroLoader.kt new file mode 100644 index 00000000..dcd3dd1d --- /dev/null +++ b/app/src/main/java/emu/skyline/loader/NroLoader.kt @@ -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 + } +} diff --git a/app/src/main/java/emu/skyline/utility/FolderActivity.kt b/app/src/main/java/emu/skyline/utility/FolderActivity.kt new file mode 100644 index 00000000..6decf7ba --- /dev/null +++ b/app/src/main/java/emu/skyline/utility/FolderActivity.kt @@ -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() + } +} diff --git a/app/src/main/java/emu/skyline/utility/FolderPreference.kt b/app/src/main/java/emu/skyline/utility/FolderPreference.kt new file mode 100644 index 00000000..3de46325 --- /dev/null +++ b/app/src/main/java/emu/skyline/utility/FolderPreference.kt @@ -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 { + 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 + } + } + } +} diff --git a/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt b/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt new file mode 100644 index 00000000..b1258277 --- /dev/null +++ b/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt @@ -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() + } +} diff --git a/app/src/main/res/layout/game_activity.xml b/app/src/main/res/layout/game_activity.xml new file mode 100644 index 00000000..9a6ace7b --- /dev/null +++ b/app/src/main/res/layout/game_activity.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index d30d57a2..fb44746d 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -19,8 +19,7 @@ -