Skip to content
This repository was archived by the owner on Oct 18, 2024. It is now read-only.

Commit 79958f9

Browse files
committed
fix: parse OpenedFilesCache manually to avoid ClassCastException in release builds (fixes #1102)
1 parent 353fd28 commit 79958f9

File tree

7 files changed

+148
-32
lines changed

7 files changed

+148
-32
lines changed

app/proguard-rules.pro

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
-dontnote **
55
-dontobfuscate
66

7+
-keepattributes Signature
8+
-keepattributes Exceptions
9+
-keepattributes *Annotation*
10+
711
-keep class javax.** { *; }
812
-keep class jdkx.** { *; }
913

@@ -35,7 +39,6 @@
3539
}
3640

3741
# EventBus
38-
-keepattributes *Annotation*
3942
-keepclassmembers class ** {
4043
@org.greenrobot.eventbus.Subscribe <methods>;
4144
}
@@ -91,19 +94,29 @@
9194
# Retrofit 2
9295
-dontwarn retrofit2.**
9396
-keep class retrofit2.** { *; }
94-
-keepattributes Signature
95-
-keepattributes Exceptions
9697

9798
-keepclasseswithmembers class * {
9899
@retrofit2.http.* <methods>;
99100
}
100101

101102
# OkHttp3
102-
-keepattributes Signature
103-
-keepattributes *Annotation*
104103
-keep class okhttp3.** { *; }
105104
-keep interface okhttp3.** { *; }
106105
-dontwarn okhttp3.**
107106

108107
# Stat uploader
109-
-keep class com.itsaky.androidide.stats.** { *; }
108+
-keep class com.itsaky.androidide.stats.** { *; }
109+
110+
# Gson
111+
-keep class * extends com.google.gson.TypeAdapter
112+
-keep class * implements com.google.gson.TypeAdapterFactory
113+
-keep class * implements com.google.gson.JsonSerializer
114+
-keep class * implements com.google.gson.JsonDeserializer
115+
116+
-keepclassmembers,allowobfuscation class * {
117+
@com.google.gson.annotations.SerializedName <fields>;
118+
}
119+
120+
## Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
121+
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
122+
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken

app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,13 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
163163

164164
try {
165165
if (viewModel.openedFilesCache != null) {
166-
viewModel.openedFilesCache?.apply {
167-
this.allFiles.forEach { openFile(File(it.filePath), it.selection) }
168-
openFile(File(selectedFile))
166+
viewModel.openedFilesCache?.let { cache ->
167+
onReadOpenedFilesCache(cache)
169168
}
170169
} else {
171-
viewModel.readOpenedFiles {
172-
it ?: return@readOpenedFiles
173-
it.allFiles.forEach { file -> openFile(File(file.filePath), file.selection) }
174-
openFile(File(it.selectedFile))
170+
viewModel.readOpenedFiles { cache ->
171+
cache ?: return@readOpenedFiles
172+
onReadOpenedFilesCache(cache)
175173
}
176174
}
177175
viewModel.openedFilesCache = null
@@ -180,6 +178,13 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
180178
}
181179
}
182180

181+
private fun onReadOpenedFilesCache(cache: OpenedFilesCache) {
182+
cache.allFiles.forEach { file ->
183+
openFile(File(file.filePath), file.selection)
184+
}
185+
openFile(File(cache.selectedFile))
186+
}
187+
183188
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
184189
ensureToolbarMenu(menu)
185190
return true

app/src/main/java/com/itsaky/androidide/models/OpenedFile.kt

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,48 @@
1717

1818
package com.itsaky.androidide.models
1919

20+
import com.google.gson.Gson
2021
import com.google.gson.annotations.SerializedName
22+
import com.google.gson.stream.JsonReader
23+
import com.itsaky.androidide.utils.ILogger
2124

2225
/**
2326
* A file that is opened in editor.
2427
*
2528
* @author Akash Yadav
2629
*/
2730
data class OpenedFile(
28-
@SerializedName("file") val filePath: String,
29-
@SerializedName("selection") var selection: Range
30-
)
31+
@SerializedName(KEY_FILE) val filePath: String,
32+
@SerializedName(KEY_SELECTION) var selection: Range
33+
) {
34+
35+
companion object {
36+
37+
private const val KEY_FILE = "file"
38+
private const val KEY_SELECTION = "selection"
39+
private val log = ILogger.newInstance("OpenedFile")
40+
41+
fun readFrom(reader: JsonReader): OpenedFile? {
42+
return try {
43+
// reader.beginObject()
44+
// var path = ""
45+
// var selection = Range.NONE
46+
// while(reader.hasNext()) {
47+
// val name = reader.nextName()
48+
// if (name == KEY_FILE) {
49+
// path = reader.nextString()
50+
// } else if (name == KEY_SELECTION) {
51+
// selection = Gson().fromJson(reader, Range::class.java)
52+
// }
53+
// }
54+
// reader.endObject()
55+
//
56+
// OpenedFile(path, selection)
57+
Gson().fromJson(reader, OpenedFile::class.java)
58+
} catch (err: Exception) {
59+
log.error("Failed to read opened file", err)
60+
null
61+
}
62+
}
63+
}
64+
}

app/src/main/java/com/itsaky/androidide/models/OpenedFilesCache.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,56 @@
1717

1818
package com.itsaky.androidide.models
1919

20+
import com.google.gson.Gson
21+
import com.google.gson.annotations.SerializedName
22+
import com.google.gson.reflect.TypeToken
23+
import com.google.gson.stream.JsonReader
24+
import com.itsaky.androidide.utils.ILogger
25+
import java.io.Reader
26+
2027
/**
2128
* @author Akash Yadav
2229
*/
23-
data class OpenedFilesCache(val selectedFile: String, val allFiles: List<OpenedFile>)
30+
data class OpenedFilesCache(@SerializedName(KEY_SELECTED_FILE) val selectedFile: String,
31+
@SerializedName(KEY_ALL_FILES) val allFiles: List<OpenedFile>) {
32+
33+
companion object {
34+
35+
private const val KEY_SELECTED_FILE = "selectedFile"
36+
private const val KEY_ALL_FILES = "allFiles"
37+
private val log = ILogger.newInstance("OpenedFilesCache")
38+
39+
@JvmStatic
40+
fun parse(contentReader: Reader): OpenedFilesCache? {
41+
return try {
42+
JsonReader(contentReader).use { reader ->
43+
if (!reader.hasNext()) {
44+
return@use null
45+
}
46+
47+
reader.beginObject()
48+
var selectedFile = ""
49+
var allFiles = emptyList<OpenedFile>()
50+
while (reader.hasNext()) {
51+
val name = reader.nextName()
52+
53+
if (name == KEY_SELECTED_FILE) {
54+
selectedFile = reader.nextString()
55+
} else if (name == KEY_ALL_FILES) {
56+
allFiles = Gson().fromJson(reader, object : TypeToken<List<OpenedFile>>() {})
57+
}
58+
}
59+
reader.endObject()
60+
61+
return@use OpenedFilesCache(selectedFile, allFiles)
62+
}
63+
} catch (err: Exception) {
64+
log.error("Failed to parse opened files cache", err)
65+
null
66+
}
67+
}
68+
69+
private fun readOpenedFiles(reader: JsonReader, allFiles: MutableList<OpenedFile>) {
70+
}
71+
}
72+
}

app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModel
2525
import com.blankj.utilcode.util.FileUtils
2626
import com.google.gson.Gson
2727
import com.google.gson.GsonBuilder
28+
import com.google.gson.reflect.TypeToken
2829
import com.itsaky.androidide.models.OpenedFilesCache
2930
import com.itsaky.androidide.projects.ProjectManager
3031
import com.itsaky.androidide.tasks.executeAsync
@@ -199,11 +200,13 @@ class EditorViewModel : ViewModel() {
199200

200201
fun readOpenedFiles(result: (OpenedFilesCache?) -> Unit) {
201202
executeAsync({
202-
val file = getOpenedFilesCache()
203+
val file = getOpenedFilesCache(false)
203204
if (file.length() == 0L) {
204205
return@executeAsync null
205206
}
206-
return@executeAsync Gson().fromJson(file.readText(), OpenedFilesCache::class.java)
207+
return@executeAsync file.reader().buffered().use { reader ->
208+
OpenedFilesCache.parse(reader)
209+
}
207210
}) {
208211
result(it)
209212
}
@@ -224,17 +227,17 @@ class EditorViewModel : ViewModel() {
224227
}
225228
}
226229

227-
private fun getOpenedFilesCache(): File {
230+
private fun getOpenedFilesCache(forWrite: Boolean = false): File {
228231
var file = Environment.getProjectCacheDir(ProjectManager.projectPath)
229232
file = File(file, "editor/openedFiles.json")
230-
if (file.exists()) {
233+
if (file.exists() && forWrite) {
231234
FileUtils.rename(file, "${file.name}.bak")
232235
}
233236

234237
if (file.parentFile?.exists() == false) {
235238
file.parentFile?.mkdirs()
236239
}
237-
240+
238241
file.createNewFile()
239242

240243
return file

shared/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ plugins {
2424
dependencies {
2525
implementation(libs.androidx.annotation)
2626
implementation(libs.google.guava)
27+
implementation(libs.google.gson)
2728
implementation(projects.logger)
2829

2930
testImplementation(libs.tests.google.truth)

shared/src/main/java/com/itsaky/androidide/models/Locations.kt

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717

1818
package com.itsaky.androidide.models
1919

20+
import com.google.gson.annotations.SerializedName
2021
import java.nio.file.Path
2122

2223
data class Location(var file: Path, var range: Range)
2324

24-
data class Position @JvmOverloads constructor(var line: Int, var column: Int, var index: Int = -1) :
25-
Comparable<Position> {
25+
data class Position @JvmOverloads constructor(
26+
@SerializedName("line") var line: Int,
27+
@SerializedName("column") var column: Int,
28+
@SerializedName("index") var index: Int = -1
29+
) : Comparable<Position> {
2630

2731
fun requireIndex(): Int {
2832
if (index == -1) {
@@ -43,7 +47,9 @@ data class Position @JvmOverloads constructor(var line: Int, var column: Int, va
4347
}
4448

4549
companion object {
46-
@JvmField val NONE = Position(-1, -1)
50+
51+
@JvmField
52+
val NONE = Position(-1, -1)
4753
}
4854

4955
override fun compareTo(other: Position): Int {
@@ -85,21 +91,26 @@ data class Position @JvmOverloads constructor(var line: Int, var column: Int, va
8591

8692
open class Range
8793
@JvmOverloads
88-
constructor(var start: Position = Position(0, 0), var end: Position = Position(0, 0)) :
89-
Comparable<Range> {
90-
91-
constructor(src: Range) : this(Position(src.start.line, src.start.column), Position(src.end.line, src.end.column))
94+
constructor(
95+
@SerializedName("start") var start: Position = Position(0, 0),
96+
@SerializedName("end") var end: Position = Position(0, 0)
97+
) : Comparable<Range> {
98+
99+
constructor(src: Range) : this(Position(src.start.line, src.start.column),
100+
Position(src.end.line, src.end.column))
92101

93102
companion object {
94-
@JvmField val NONE = Range(Position.NONE, Position.NONE)
103+
104+
@JvmField
105+
val NONE = Range(Position.NONE, Position.NONE)
95106

96107
@JvmStatic
97-
fun pointRange(line: Int, column: Int) : Range {
108+
fun pointRange(line: Int, column: Int): Range {
98109
return pointRange(Position(line, column))
99110
}
100111

101112
@JvmStatic
102-
fun pointRange(position: Position) : Range {
113+
fun pointRange(position: Position): Range {
103114
return Range(position, position)
104115
}
105116
}

0 commit comments

Comments
 (0)