Skip to content

Commit 99f79d9

Browse files
[shared_preferences] Fix Android type mismatch regression (#8512)
`getStringList` should throw a `TypeError` if the stored value is of a different type, but the recent change to use JSON-encoded string lists regression that behavior if the stored type was a string, causing it to instead return null. This restores the previous behavior by passing extra information from Kotlin to Dart when attempting to get an enecoded string list, so that if a non-encoded-list string is found, a TypeError can be created on the Dart side. Since extra information is now being passed, the case of a legacy-encoded value is now communicated as well, so that we only have to request the legacy value if it's there, rather than always trying (which was not worth the complexity of adding extra data just for that initially, but now that we need extra data anyway, it's easy to distinguish that case). Fixes OOB regression in `shared_preferences` tests that has closed the tree.
1 parent 258f6dc commit 99f79d9

File tree

10 files changed

+261
-38
lines changed

10 files changed

+261
-38
lines changed

packages/shared_preferences/shared_preferences_android/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.4.4
2+
3+
* Restores the behavior of throwing a `TypeError` when calling `getStringList`
4+
on a value stored with `setString`.
5+
16
## 2.4.3
27

38
* Migrates `List<String>` value encoding to JSON.

packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
4+
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
55
// See also: https://pub.dev/packages/pigeon
66
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
77

@@ -43,6 +43,22 @@ class SharedPreferencesError(
4343
val details: Any? = null
4444
) : Throwable()
4545

46+
/** Possible types found during a getStringList call. */
47+
enum class StringListLookupResultType(val raw: Int) {
48+
/** A deprecated platform-side encoding string list. */
49+
PLATFORM_ENCODED(0),
50+
/** A JSON-encoded string list. */
51+
JSON_ENCODED(1),
52+
/** A string that doesn't have the expected encoding prefix. */
53+
UNEXPECTED_STRING(2);
54+
55+
companion object {
56+
fun ofRaw(raw: Int): StringListLookupResultType? {
57+
return values().firstOrNull { it.raw == raw }
58+
}
59+
}
60+
}
61+
4662
/** Generated class from Pigeon that represents data sent in messages. */
4763
data class SharedPreferencesPigeonOptions(val fileName: String? = null, val useDataStore: Boolean) {
4864
companion object {
@@ -61,22 +77,59 @@ data class SharedPreferencesPigeonOptions(val fileName: String? = null, val useD
6177
}
6278
}
6379

80+
/** Generated class from Pigeon that represents data sent in messages. */
81+
data class StringListResult(
82+
/** The JSON-encoded stored value, or null if something else was found. */
83+
val jsonEncodedValue: String? = null,
84+
/** The type of value found. */
85+
val type: StringListLookupResultType
86+
) {
87+
companion object {
88+
fun fromList(pigeonVar_list: List<Any?>): StringListResult {
89+
val jsonEncodedValue = pigeonVar_list[0] as String?
90+
val type = pigeonVar_list[1] as StringListLookupResultType
91+
return StringListResult(jsonEncodedValue, type)
92+
}
93+
}
94+
95+
fun toList(): List<Any?> {
96+
return listOf(
97+
jsonEncodedValue,
98+
type,
99+
)
100+
}
101+
}
102+
64103
private open class MessagesAsyncPigeonCodec : StandardMessageCodec() {
65104
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
66105
return when (type) {
67106
129.toByte() -> {
107+
return (readValue(buffer) as Long?)?.let { StringListLookupResultType.ofRaw(it.toInt()) }
108+
}
109+
130.toByte() -> {
68110
return (readValue(buffer) as? List<Any?>)?.let {
69111
SharedPreferencesPigeonOptions.fromList(it)
70112
}
71113
}
114+
131.toByte() -> {
115+
return (readValue(buffer) as? List<Any?>)?.let { StringListResult.fromList(it) }
116+
}
72117
else -> super.readValueOfType(type, buffer)
73118
}
74119
}
75120

76121
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
77122
when (value) {
78-
is SharedPreferencesPigeonOptions -> {
123+
is StringListLookupResultType -> {
79124
stream.write(129)
125+
writeValue(stream, value.raw)
126+
}
127+
is SharedPreferencesPigeonOptions -> {
128+
stream.write(130)
129+
writeValue(stream, value.toList())
130+
}
131+
is StringListResult -> {
132+
stream.write(131)
80133
writeValue(stream, value.toList())
81134
}
82135
else -> super.writeValue(stream, value)
@@ -119,8 +172,8 @@ interface SharedPreferencesAsyncApi {
119172
key: String,
120173
options: SharedPreferencesPigeonOptions
121174
): List<String>?
122-
/** Gets individual List<String> value stored with [key], if any. */
123-
fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String?
175+
/** Gets the JSON-encoded List<String> value stored with [key], if any. */
176+
fun getStringList(key: String, options: SharedPreferencesPigeonOptions): StringListResult?
124177
/** Removes all properties from shared preferences data set with matching prefix. */
125178
fun clear(allowList: List<String>?, options: SharedPreferencesPigeonOptions)
126179
/** Gets all properties from shared preferences data set with matching prefix. */

packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,20 @@ class SharedPreferencesPlugin() : FlutterPlugin, SharedPreferencesAsyncApi {
203203
}
204204

205205
/** Gets StringList at [key] from data store. */
206-
override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? {
206+
override fun getStringList(
207+
key: String,
208+
options: SharedPreferencesPigeonOptions
209+
): StringListResult? {
207210
val stringValue = getString(key, options)
208211
stringValue?.let {
209212
// The JSON-encoded lists use an extended prefix to distinguish them from
210213
// lists that using listEncoder.
211-
if (stringValue.startsWith(JSON_LIST_PREFIX)) {
212-
return stringValue
214+
return if (stringValue.startsWith(JSON_LIST_PREFIX)) {
215+
StringListResult(stringValue, StringListLookupResultType.JSON_ENCODED)
216+
} else if (stringValue.startsWith(LIST_PREFIX)) {
217+
StringListResult(null, StringListLookupResultType.PLATFORM_ENCODED)
218+
} else {
219+
StringListResult(null, StringListLookupResultType.UNEXPECTED_STRING)
213220
}
214221
}
215222
return null
@@ -408,12 +415,21 @@ class SharedPreferencesBackend(
408415
}
409416

410417
/** Gets StringList at [key] from data store. */
411-
override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? {
418+
override fun getStringList(
419+
key: String,
420+
options: SharedPreferencesPigeonOptions
421+
): StringListResult? {
412422
val preferences = createSharedPreferences(options)
413423
if (preferences.contains(key)) {
414424
val value = preferences.getString(key, "")
415-
if (value!!.startsWith(JSON_LIST_PREFIX)) {
416-
return value
425+
// The JSON-encoded lists use an extended prefix to distinguish them from
426+
// lists that using listEncoder.
427+
return if (value!!.startsWith(JSON_LIST_PREFIX)) {
428+
StringListResult(value, StringListLookupResultType.JSON_ENCODED)
429+
} else if (value.startsWith(LIST_PREFIX)) {
430+
StringListResult(null, StringListLookupResultType.PLATFORM_ENCODED)
431+
} else {
432+
StringListResult(null, StringListLookupResultType.UNEXPECTED_STRING)
417433
}
418434
}
419435
return null

packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,27 @@ internal class SharedPreferencesTest {
9696
fun testSetAndGetStringListWithDataStore() {
9797
val plugin = pluginSetup(dataStoreOptions)
9898
plugin.setEncodedStringList(listKey, testList, dataStoreOptions)
99-
Assert.assertEquals(plugin.getStringList(listKey, dataStoreOptions), testList)
99+
val result = plugin.getStringList(listKey, dataStoreOptions)
100+
Assert.assertEquals(result?.jsonEncodedValue, testList)
101+
Assert.assertEquals(result?.type, StringListLookupResultType.JSON_ENCODED)
102+
}
103+
104+
@Test
105+
fun testSetAndGetStringListWithDataStoreRedirectsForPlatformEncoded() {
106+
val plugin = pluginSetup(dataStoreOptions)
107+
plugin.setDeprecatedStringList(listKey, listOf(""), dataStoreOptions)
108+
val result = plugin.getStringList(listKey, dataStoreOptions)
109+
Assert.assertEquals(result?.jsonEncodedValue, null)
110+
Assert.assertEquals(result?.type, StringListLookupResultType.PLATFORM_ENCODED)
111+
}
112+
113+
@Test
114+
fun testSetAndGetStringListWithDataStoreReportsRawString() {
115+
val plugin = pluginSetup(dataStoreOptions)
116+
plugin.setString(listKey, testString, dataStoreOptions)
117+
val result = plugin.getStringList(listKey, dataStoreOptions)
118+
Assert.assertEquals(result?.jsonEncodedValue, null)
119+
Assert.assertEquals(result?.type, StringListLookupResultType.UNEXPECTED_STRING)
100120
}
101121

102122
@Test
@@ -217,7 +237,27 @@ internal class SharedPreferencesTest {
217237
fun testSetAndGetStringListWithSharedPreferences() {
218238
val plugin = pluginSetup(sharedPreferencesOptions)
219239
plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions)
220-
Assert.assertEquals(plugin.getStringList(listKey, sharedPreferencesOptions), testList)
240+
val result = plugin.getStringList(listKey, sharedPreferencesOptions)
241+
Assert.assertEquals(result?.jsonEncodedValue, testList)
242+
Assert.assertEquals(result?.type, StringListLookupResultType.JSON_ENCODED)
243+
}
244+
245+
@Test
246+
fun testSetAndGetStringListWithSharedPreferencesRedirectsForPlatformEncoded() {
247+
val plugin = pluginSetup(sharedPreferencesOptions)
248+
plugin.setDeprecatedStringList(listKey, listOf(""), sharedPreferencesOptions)
249+
val result = plugin.getStringList(listKey, sharedPreferencesOptions)
250+
Assert.assertEquals(result?.jsonEncodedValue, null)
251+
Assert.assertEquals(result?.type, StringListLookupResultType.PLATFORM_ENCODED)
252+
}
253+
254+
@Test
255+
fun testSetAndGetStringListWithSharedPreferencesReportsRawString() {
256+
val plugin = pluginSetup(sharedPreferencesOptions)
257+
plugin.setString(listKey, testString, sharedPreferencesOptions)
258+
val result = plugin.getStringList(listKey, sharedPreferencesOptions)
259+
Assert.assertEquals(result?.jsonEncodedValue, null)
260+
Assert.assertEquals(result?.type, StringListLookupResultType.UNEXPECTED_STRING)
221261
}
222262

223263
@Test

packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,22 @@ void main() {
639639
expect(list?.length, testList.length + 1);
640640
});
641641

642+
testWidgets('getStringList throws type error for String with $backend',
643+
(WidgetTester _) async {
644+
final SharedPreferencesAsyncAndroidOptions options =
645+
getOptions(useDataStore: useDataStore, fileName: 'notDefault');
646+
final SharedPreferencesAsyncPlatform preferences = getPreferences();
647+
await clearPreferences(preferences, options);
648+
649+
await preferences.setString(listKey, testString, options);
650+
651+
// Internally, List<String> is stored as a String on Android, but that
652+
// implementation detail shouldn't leak to clients; getting the wrong
653+
// type should still throw.
654+
expect(preferences.getStringList(listKey, options),
655+
throwsA(isA<TypeError>()));
656+
});
657+
642658
testWidgets('getPreferences with $backend', (WidgetTester _) async {
643659
final SharedPreferencesAsyncAndroidOptions options =
644660
getOptions(useDataStore: useDataStore, fileName: 'notDefault');

packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
4+
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
55
// See also: https://pub.dev/packages/pigeon
66
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
77

@@ -18,6 +18,18 @@ PlatformException _createConnectionError(String channelName) {
1818
);
1919
}
2020

21+
/// Possible types found during a getStringList call.
22+
enum StringListLookupResultType {
23+
/// A deprecated platform-side encoding string list.
24+
platformEncoded,
25+
26+
/// A JSON-encoded string list.
27+
jsonEncoded,
28+
29+
/// A string that doesn't have the expected encoding prefix.
30+
unexpectedString,
31+
}
32+
2133
class SharedPreferencesPigeonOptions {
2234
SharedPreferencesPigeonOptions({
2335
this.fileName,
@@ -44,15 +56,49 @@ class SharedPreferencesPigeonOptions {
4456
}
4557
}
4658

59+
class StringListResult {
60+
StringListResult({
61+
this.jsonEncodedValue,
62+
required this.type,
63+
});
64+
65+
/// The JSON-encoded stored value, or null if something else was found.
66+
String? jsonEncodedValue;
67+
68+
/// The type of value found.
69+
StringListLookupResultType type;
70+
71+
Object encode() {
72+
return <Object?>[
73+
jsonEncodedValue,
74+
type,
75+
];
76+
}
77+
78+
static StringListResult decode(Object result) {
79+
result as List<Object?>;
80+
return StringListResult(
81+
jsonEncodedValue: result[0] as String?,
82+
type: result[1]! as StringListLookupResultType,
83+
);
84+
}
85+
}
86+
4787
class _PigeonCodec extends StandardMessageCodec {
4888
const _PigeonCodec();
4989
@override
5090
void writeValue(WriteBuffer buffer, Object? value) {
5191
if (value is int) {
5292
buffer.putUint8(4);
5393
buffer.putInt64(value);
54-
} else if (value is SharedPreferencesPigeonOptions) {
94+
} else if (value is StringListLookupResultType) {
5595
buffer.putUint8(129);
96+
writeValue(buffer, value.index);
97+
} else if (value is SharedPreferencesPigeonOptions) {
98+
buffer.putUint8(130);
99+
writeValue(buffer, value.encode());
100+
} else if (value is StringListResult) {
101+
buffer.putUint8(131);
56102
writeValue(buffer, value.encode());
57103
} else {
58104
super.writeValue(buffer, value);
@@ -63,7 +109,12 @@ class _PigeonCodec extends StandardMessageCodec {
63109
Object? readValueOfType(int type, ReadBuffer buffer) {
64110
switch (type) {
65111
case 129:
112+
final int? value = readValue(buffer) as int?;
113+
return value == null ? null : StringListLookupResultType.values[value];
114+
case 130:
66115
return SharedPreferencesPigeonOptions.decode(readValue(buffer)!);
116+
case 131:
117+
return StringListResult.decode(readValue(buffer)!);
67118
default:
68119
return super.readValueOfType(type, buffer);
69120
}
@@ -373,8 +424,8 @@ class SharedPreferencesAsyncApi {
373424
}
374425
}
375426

376-
/// Gets individual List<String> value stored with [key], if any.
377-
Future<String?> getStringList(
427+
/// Gets the JSON-encoded List<String> value stored with [key], if any.
428+
Future<StringListResult?> getStringList(
378429
String key, SharedPreferencesPigeonOptions options) async {
379430
final String pigeonVar_channelName =
380431
'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.getStringList$pigeonVar_messageChannelSuffix';
@@ -395,7 +446,7 @@ class SharedPreferencesAsyncApi {
395446
details: pigeonVar_replyList[2],
396447
);
397448
} else {
398-
return (pigeonVar_replyList[0] as String?);
449+
return (pigeonVar_replyList[0] as StringListResult?);
399450
}
400451
}
401452

0 commit comments

Comments
 (0)