Skip to content

Commit ad6b40c

Browse files
david-allisonmikehardy
authored andcommitted
fix(release): Unable to start activity: MultimediaActivity
Reverts b73ac85 We got the following truncated stack trace due to proguard/minify Fix it via implementing Parcelize/Parcelable ``` java.lang.RuntimeException: Unable to start activity ComponentInfo{com.ichi2.anki/com.ichi2.anki.multimedia.MultimediaActivity}: android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object (name = com.ichi2.anki.multimedia.f)     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4377)     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4574)     ...     at android.os.Looper.loop(Looper.java:393)     at android.app.ActivityThread.main(ActivityThread.java:9549)     at java.lang.reflect.Method.invoke(Native Method)     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:600)     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1005) Caused by: android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object (name = com.ichi2.anki.multimedia.f)     at android.os.Parcel.readSerializableInternal(Parcel.java:5410)     at android.os.Parcel.readValue(Parcel.java:4928)     at android.os.Parcel.readValue(Parcel.java:4625)     ...     at android.os.Bundle.getSerializable(Bundle.java:1283)     at com.ichi2.compat.CompatV33.getSerializable(CompatV33.kt:47)     at com.ichi2.compat.CompatHelper$Companion.getSerializableCompat(CompatHelper.java:85)     at com.ichi2.anki.multimedia.MultimediaActivity.getMultimediaArgsExtra(MultimediaActivity.kt:66)     at com.ichi2.anki.multimedia.MultimediaActivity.onCreate(MultimediaActivity.kt:96)     at android.app.Activity.performCreate(Activity.java:9196) Caused by: java.io.InvalidClassException: m4.d; no valid constructor     at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:163)     at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:832) ``` Fixes 18712 (cherry picked from commit 4806e76)
1 parent 3fd6804 commit ad6b40c

File tree

5 files changed

+232
-6
lines changed

5 files changed

+232
-6
lines changed

AnkiDroid/proguard-rules.pro

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@
2525

2626
# Used through Reflection
2727
-keep class com.ichi2.anki.**.*Fragment { *; }
28-
# 18712: MultimediaActivity: android.os.BadParcelableException
29-
# TODO: this is brittle; IMultimediaEditableNote should implement Parcelable
30-
-keep class com.ichi2.anki.**.multimediacard.** { *; }
3128
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
3229
-keep class androidx.core.app.ActivityCompat$* { *; }
3330
-keep class androidx.concurrent.futures.** { *; }

AnkiDroid/src/main/java/com/ichi2/anki/multimediacard/impl/MultimediaEditableNote.kt

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,42 @@
1919

2020
package com.ichi2.anki.multimediacard.impl
2121

22+
import android.os.Parcel
23+
import android.os.Parcelable
2224
import com.ichi2.anki.multimediacard.IMultimediaEditableNote
2325
import com.ichi2.anki.multimediacard.fields.IField
26+
import com.ichi2.anki.utils.ext.readSerializableList
27+
import com.ichi2.anki.utils.ext.writeSerializableList
28+
import com.ichi2.compat.readBooleanCompat
29+
import com.ichi2.compat.writeBooleanCompat
2430
import com.ichi2.libanki.NoteTypeId
31+
import kotlinx.parcelize.Parceler
32+
import kotlinx.parcelize.Parcelize
2533
import org.acra.util.IOUtils
26-
import java.util.ArrayList
2734

2835
/**
2936
* Implementation of the editable note.
30-
* <p>
37+
*
3138
* Has to be translate to and from anki db format.
39+
*
40+
* All variables must be handled manually by Parcelable
3241
*/
33-
class MultimediaEditableNote : IMultimediaEditableNote {
42+
@Parcelize
43+
class MultimediaEditableNote() :
44+
IMultimediaEditableNote,
45+
Parcelable {
46+
internal constructor(
47+
isModified: Boolean,
48+
noteTypeId: Long,
49+
initialFields: List<IField?>?,
50+
fields: List<IField?>?,
51+
) : this() {
52+
this.isModified = isModified
53+
this.noteTypeId = noteTypeId
54+
this.initialFields = initialFields?.let { ArrayList(it) }
55+
this.fields = fields?.let { ArrayList(it) }
56+
}
57+
3458
override var isModified = false
3559
private set
3660
private var fields: ArrayList<IField?>? = null
@@ -106,4 +130,24 @@ class MultimediaEditableNote : IMultimediaEditableNote {
106130

107131
val isEmpty: Boolean
108132
get() = fields.isNullOrEmpty()
133+
134+
companion object : Parceler<MultimediaEditableNote> {
135+
override fun create(parcel: Parcel): MultimediaEditableNote =
136+
MultimediaEditableNote(
137+
isModified = parcel.readBooleanCompat(),
138+
noteTypeId = parcel.readLong(),
139+
initialFields = parcel.readSerializableList<IField>(),
140+
fields = parcel.readSerializableList<IField>(),
141+
)
142+
143+
override fun MultimediaEditableNote.write(
144+
parcel: Parcel,
145+
flags: Int,
146+
) {
147+
parcel.writeBooleanCompat(isModified)
148+
parcel.writeLong(noteTypeId)
149+
parcel.writeSerializableList<IField>(initialFields)
150+
parcel.writeSerializableList<IField>(fields)
151+
}
152+
}
109153
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2025 David Allison <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki.utils.ext
18+
19+
import android.os.Parcel
20+
import androidx.core.os.ParcelCompat
21+
import java.io.Serializable
22+
23+
fun <T : Serializable> Parcel.writeSerializableList(list: List<T?>?) {
24+
if (list == null) {
25+
writeInt(-1)
26+
return
27+
}
28+
writeInt(list.size)
29+
for (item in list) {
30+
if (item == null) {
31+
writeInt(0)
32+
continue
33+
}
34+
writeInt(1)
35+
writeSerializable(item)
36+
}
37+
}
38+
39+
inline fun <reified T : Serializable> Parcel.readSerializableList(): List<T?>? {
40+
val size = readInt()
41+
if (size == -1) return null
42+
return List(size = size) {
43+
if (readInt() == 0) {
44+
null
45+
} else {
46+
ParcelCompat.readSerializable(
47+
this,
48+
T::class.java.classLoader,
49+
T::class.java,
50+
)
51+
}
52+
}
53+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2025 David Allison <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.compat
18+
19+
import android.os.Parcel
20+
21+
// writeBoolean requires API level 29
22+
fun Parcel.writeBooleanCompat(value: Boolean) {
23+
writeByte(if (value) 1 else 0)
24+
}
25+
26+
// readBoolean requires API level 29
27+
fun Parcel.readBooleanCompat() = readByte() != 0.toByte()
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright (c) 2025 David Allison <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki.utils.ext
18+
19+
import android.os.Parcel
20+
import androidx.test.espresso.matcher.ViewMatchers.assertThat
21+
import androidx.test.ext.junit.runners.AndroidJUnit4
22+
import com.ichi2.anki.utils.ext.ParcelableUtilsTest.UnderTest
23+
import com.ichi2.anki.utils.ext.ParcelableUtilsTest.UnderTest.Companion.write
24+
import com.ichi2.anki.utils.ext.ParcelableUtilsTest.UnderTest.User
25+
import com.ichi2.testutils.EmptyApplication
26+
import kotlinx.parcelize.Parceler
27+
import org.hamcrest.Matchers.equalTo
28+
import org.hamcrest.Matchers.hasSize
29+
import org.hamcrest.Matchers.nullValue
30+
import org.junit.Test
31+
import org.junit.runner.RunWith
32+
import org.robolectric.annotation.Config
33+
import java.io.Serializable
34+
35+
@RunWith(AndroidJUnit4::class)
36+
@Config(application = EmptyApplication::class)
37+
class ParcelableUtilsTest {
38+
@Test
39+
fun `serializableList - valid`() {
40+
val withData =
41+
UnderTest().apply {
42+
list =
43+
listOf(
44+
User(1, "david"),
45+
null,
46+
User(2, "dave"),
47+
)
48+
}
49+
50+
val clonedList = withData.cloneAsParcel().list!!
51+
52+
assertThat(clonedList, hasSize(3))
53+
clonedList[0]!!.let {
54+
assertThat(it.id, equalTo(1))
55+
assertThat(it.name, equalTo("david"))
56+
}
57+
assertThat(clonedList[1], nullValue())
58+
clonedList[2]!!.let {
59+
assertThat(it.id, equalTo(2))
60+
assertThat(it.name, equalTo("dave"))
61+
}
62+
}
63+
64+
@Test
65+
fun `serializableList - null list`() {
66+
val withData =
67+
UnderTest().apply {
68+
list = null
69+
}
70+
71+
assertThat(withData.cloneAsParcel().list, nullValue())
72+
}
73+
74+
class UnderTest {
75+
var list: List<User?>? = null
76+
77+
data class User(
78+
val id: Int,
79+
val name: String,
80+
) : Serializable
81+
82+
companion object : Parceler<UnderTest> {
83+
override fun create(parcel: Parcel): UnderTest =
84+
UnderTest().apply {
85+
list = parcel.readSerializableList<User>()
86+
}
87+
88+
override fun UnderTest.write(
89+
parcel: Parcel,
90+
flags: Int,
91+
) {
92+
parcel.writeSerializableList(list)
93+
}
94+
}
95+
}
96+
}
97+
98+
fun UnderTest.cloneAsParcel(): UnderTest {
99+
val parcel = Parcel.obtain()
100+
this.write(parcel, 0)
101+
parcel.setDataPosition(0)
102+
return UnderTest.create(parcel).apply {
103+
parcel.recycle()
104+
}
105+
}

0 commit comments

Comments
 (0)