fix: parse OpenedFilesCache manually to avoid ClassCastException in release builds (fixes #1102)

This commit is contained in:
Akash Yadav
2023-07-14 17:37:33 +05:30
parent 353fd287ba
commit 79958f9d9f
7 changed files with 148 additions and 32 deletions
+19 -6
View File
@@ -4,6 +4,10 @@
-dontnote **
-dontobfuscate
-keepattributes Signature
-keepattributes Exceptions
-keepattributes *Annotation*
-keep class javax.** { *; }
-keep class jdkx.** { *; }
@@ -35,7 +39,6 @@
}
# EventBus
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
@@ -91,19 +94,29 @@
# Retrofit 2
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# OkHttp3
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
# Stat uploader
-keep class com.itsaky.androidide.stats.** { *; }
-keep class com.itsaky.androidide.stats.** { *; }
# Gson
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
## Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
@@ -163,15 +163,13 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
try {
if (viewModel.openedFilesCache != null) {
viewModel.openedFilesCache?.apply {
this.allFiles.forEach { openFile(File(it.filePath), it.selection) }
openFile(File(selectedFile))
viewModel.openedFilesCache?.let { cache ->
onReadOpenedFilesCache(cache)
}
} else {
viewModel.readOpenedFiles {
it ?: return@readOpenedFiles
it.allFiles.forEach { file -> openFile(File(file.filePath), file.selection) }
openFile(File(it.selectedFile))
viewModel.readOpenedFiles { cache ->
cache ?: return@readOpenedFiles
onReadOpenedFilesCache(cache)
}
}
viewModel.openedFilesCache = null
@@ -180,6 +178,13 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
}
}
private fun onReadOpenedFilesCache(cache: OpenedFilesCache) {
cache.allFiles.forEach { file ->
openFile(File(file.filePath), file.selection)
}
openFile(File(cache.selectedFile))
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ensureToolbarMenu(menu)
return true
@@ -17,7 +17,10 @@
package com.itsaky.androidide.models
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.google.gson.stream.JsonReader
import com.itsaky.androidide.utils.ILogger
/**
* A file that is opened in editor.
@@ -25,6 +28,37 @@ import com.google.gson.annotations.SerializedName
* @author Akash Yadav
*/
data class OpenedFile(
@SerializedName("file") val filePath: String,
@SerializedName("selection") var selection: Range
)
@SerializedName(KEY_FILE) val filePath: String,
@SerializedName(KEY_SELECTION) var selection: Range
) {
companion object {
private const val KEY_FILE = "file"
private const val KEY_SELECTION = "selection"
private val log = ILogger.newInstance("OpenedFile")
fun readFrom(reader: JsonReader): OpenedFile? {
return try {
// reader.beginObject()
// var path = ""
// var selection = Range.NONE
// while(reader.hasNext()) {
// val name = reader.nextName()
// if (name == KEY_FILE) {
// path = reader.nextString()
// } else if (name == KEY_SELECTION) {
// selection = Gson().fromJson(reader, Range::class.java)
// }
// }
// reader.endObject()
//
// OpenedFile(path, selection)
Gson().fromJson(reader, OpenedFile::class.java)
} catch (err: Exception) {
log.error("Failed to read opened file", err)
null
}
}
}
}
@@ -17,7 +17,56 @@
package com.itsaky.androidide.models
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.itsaky.androidide.utils.ILogger
import java.io.Reader
/**
* @author Akash Yadav
*/
data class OpenedFilesCache(val selectedFile: String, val allFiles: List<OpenedFile>)
data class OpenedFilesCache(@SerializedName(KEY_SELECTED_FILE) val selectedFile: String,
@SerializedName(KEY_ALL_FILES) val allFiles: List<OpenedFile>) {
companion object {
private const val KEY_SELECTED_FILE = "selectedFile"
private const val KEY_ALL_FILES = "allFiles"
private val log = ILogger.newInstance("OpenedFilesCache")
@JvmStatic
fun parse(contentReader: Reader): OpenedFilesCache? {
return try {
JsonReader(contentReader).use { reader ->
if (!reader.hasNext()) {
return@use null
}
reader.beginObject()
var selectedFile = ""
var allFiles = emptyList<OpenedFile>()
while (reader.hasNext()) {
val name = reader.nextName()
if (name == KEY_SELECTED_FILE) {
selectedFile = reader.nextString()
} else if (name == KEY_ALL_FILES) {
allFiles = Gson().fromJson(reader, object : TypeToken<List<OpenedFile>>() {})
}
}
reader.endObject()
return@use OpenedFilesCache(selectedFile, allFiles)
}
} catch (err: Exception) {
log.error("Failed to parse opened files cache", err)
null
}
}
private fun readOpenedFiles(reader: JsonReader, allFiles: MutableList<OpenedFile>) {
}
}
}
@@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModel
import com.blankj.utilcode.util.FileUtils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.tasks.executeAsync
@@ -199,11 +200,13 @@ class EditorViewModel : ViewModel() {
fun readOpenedFiles(result: (OpenedFilesCache?) -> Unit) {
executeAsync({
val file = getOpenedFilesCache()
val file = getOpenedFilesCache(false)
if (file.length() == 0L) {
return@executeAsync null
}
return@executeAsync Gson().fromJson(file.readText(), OpenedFilesCache::class.java)
return@executeAsync file.reader().buffered().use { reader ->
OpenedFilesCache.parse(reader)
}
}) {
result(it)
}
@@ -224,17 +227,17 @@ class EditorViewModel : ViewModel() {
}
}
private fun getOpenedFilesCache(): File {
private fun getOpenedFilesCache(forWrite: Boolean = false): File {
var file = Environment.getProjectCacheDir(ProjectManager.projectPath)
file = File(file, "editor/openedFiles.json")
if (file.exists()) {
if (file.exists() && forWrite) {
FileUtils.rename(file, "${file.name}.bak")
}
if (file.parentFile?.exists() == false) {
file.parentFile?.mkdirs()
}
file.createNewFile()
return file
+1
View File
@@ -24,6 +24,7 @@ plugins {
dependencies {
implementation(libs.androidx.annotation)
implementation(libs.google.guava)
implementation(libs.google.gson)
implementation(projects.logger)
testImplementation(libs.tests.google.truth)
@@ -17,12 +17,16 @@
package com.itsaky.androidide.models
import com.google.gson.annotations.SerializedName
import java.nio.file.Path
data class Location(var file: Path, var range: Range)
data class Position @JvmOverloads constructor(var line: Int, var column: Int, var index: Int = -1) :
Comparable<Position> {
data class Position @JvmOverloads constructor(
@SerializedName("line") var line: Int,
@SerializedName("column") var column: Int,
@SerializedName("index") var index: Int = -1
) : Comparable<Position> {
fun requireIndex(): Int {
if (index == -1) {
@@ -43,7 +47,9 @@ data class Position @JvmOverloads constructor(var line: Int, var column: Int, va
}
companion object {
@JvmField val NONE = Position(-1, -1)
@JvmField
val NONE = Position(-1, -1)
}
override fun compareTo(other: Position): Int {
@@ -85,21 +91,26 @@ data class Position @JvmOverloads constructor(var line: Int, var column: Int, va
open class Range
@JvmOverloads
constructor(var start: Position = Position(0, 0), var end: Position = Position(0, 0)) :
Comparable<Range> {
constructor(src: Range) : this(Position(src.start.line, src.start.column), Position(src.end.line, src.end.column))
constructor(
@SerializedName("start") var start: Position = Position(0, 0),
@SerializedName("end") var end: Position = Position(0, 0)
) : Comparable<Range> {
constructor(src: Range) : this(Position(src.start.line, src.start.column),
Position(src.end.line, src.end.column))
companion object {
@JvmField val NONE = Range(Position.NONE, Position.NONE)
@JvmField
val NONE = Range(Position.NONE, Position.NONE)
@JvmStatic
fun pointRange(line: Int, column: Int) : Range {
fun pointRange(line: Int, column: Int): Range {
return pointRange(Position(line, column))
}
@JvmStatic
fun pointRange(position: Position) : Range {
fun pointRange(position: Position): Range {
return Range(position, position)
}
}