From bbadd77e380a006a6f5c1ef93dd2a5e059a5afc2 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 27 Jan 2025 15:13:11 -0500 Subject: [PATCH 1/2] [shared_preferences] Fix Android type mismatch regression `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. --- .../shared_preferences_android/CHANGELOG.md | 5 ++ .../sharedpreferences/MessagesAsync.g.kt | 46 ++++++++++++++++-- .../SharedPreferencesPlugin.kt | 28 ++++++++--- .../SharedPreferencesTest.kt | 42 +++++++++++++++- .../shared_preferences_test.dart | 16 +++++++ .../lib/src/messages_async.g.dart | 48 +++++++++++++++++-- .../src/shared_preferences_async_android.dart | 22 ++++++--- .../pigeons/messages_async.dart | 25 +++++++++- .../shared_preferences_android/pubspec.yaml | 2 +- .../test/shared_preferences_async_test.dart | 9 +++- 10 files changed, 217 insertions(+), 26 deletions(-) diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index 0e2bd32a07f5..8e319e40f160 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.4.4 + +* Restores the behavior of throwing a `TypeError` when calling `getStringList` + on a value stored with `setString`. + ## 2.4.3 * Migrates `List` value encoding to JSON. diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt index 44c37bad67da..f033428022f0 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.7.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -61,6 +61,39 @@ data class SharedPreferencesPigeonOptions(val fileName: String? = null, val useD } } +/** Generated class from Pigeon that represents data sent in messages. */ +data class StringListResult( + /** + * The JSON-encoded stored value, or null if there isn't one. + * + * This will be null if either there is no store value (in which case + * [foundPlatformEncodedValue] will be false), or if there was a platform-encoded value. + */ + val jsonEncodedValue: String? = null, + /** + * Whether value using the legacy platform-side encoding was found. + * + * If this is true, [jsonEncodedValue] will be null, and the value should be fetched with + * getPlatformEncodedStringList(...) instead. + */ + val foundPlatformEncodedValue: Boolean +) { + companion object { + fun fromList(pigeonVar_list: List): StringListResult { + val jsonEncodedValue = pigeonVar_list[0] as String? + val foundPlatformEncodedValue = pigeonVar_list[1] as Boolean + return StringListResult(jsonEncodedValue, foundPlatformEncodedValue) + } + } + + fun toList(): List { + return listOf( + jsonEncodedValue, + foundPlatformEncodedValue, + ) + } +} + private open class MessagesAsyncPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -69,6 +102,9 @@ private open class MessagesAsyncPigeonCodec : StandardMessageCodec() { SharedPreferencesPigeonOptions.fromList(it) } } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { StringListResult.fromList(it) } + } else -> super.readValueOfType(type, buffer) } } @@ -79,6 +115,10 @@ private open class MessagesAsyncPigeonCodec : StandardMessageCodec() { stream.write(129) writeValue(stream, value.toList()) } + is StringListResult -> { + stream.write(130) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -119,8 +159,8 @@ interface SharedPreferencesAsyncApi { key: String, options: SharedPreferencesPigeonOptions ): List? - /** Gets individual List value stored with [key], if any. */ - fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? + /** Gets the JSON-encoded List value stored with [key], if any. */ + fun getStringList(key: String, options: SharedPreferencesPigeonOptions): StringListResult? /** Removes all properties from shared preferences data set with matching prefix. */ fun clear(allowList: List?, options: SharedPreferencesPigeonOptions) /** Gets all properties from shared preferences data set with matching prefix. */ diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt index 4587c68bab6f..98a125930224 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt @@ -203,13 +203,20 @@ class SharedPreferencesPlugin() : FlutterPlugin, SharedPreferencesAsyncApi { } /** Gets StringList at [key] from data store. */ - override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? { + override fun getStringList( + key: String, + options: SharedPreferencesPigeonOptions + ): StringListResult? { val stringValue = getString(key, options) stringValue?.let { // The JSON-encoded lists use an extended prefix to distinguish them from // lists that using listEncoder. - if (stringValue.startsWith(JSON_LIST_PREFIX)) { - return stringValue + return if (stringValue.startsWith(JSON_LIST_PREFIX)) { + StringListResult(stringValue, foundPlatformEncodedValue = false) + } else if (stringValue.startsWith(LIST_PREFIX)) { + StringListResult(null, foundPlatformEncodedValue = true) + } else { + StringListResult(null, foundPlatformEncodedValue = false) } } return null @@ -408,12 +415,21 @@ class SharedPreferencesBackend( } /** Gets StringList at [key] from data store. */ - override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? { + override fun getStringList( + key: String, + options: SharedPreferencesPigeonOptions + ): StringListResult? { val preferences = createSharedPreferences(options) if (preferences.contains(key)) { val value = preferences.getString(key, "") - if (value!!.startsWith(JSON_LIST_PREFIX)) { - return value + // The JSON-encoded lists use an extended prefix to distinguish them from + // lists that using listEncoder. + return if (value!!.startsWith(JSON_LIST_PREFIX)) { + StringListResult(value, foundPlatformEncodedValue = false) + } else if (value.startsWith(LIST_PREFIX)) { + StringListResult(null, foundPlatformEncodedValue = true) + } else { + StringListResult(null, foundPlatformEncodedValue = false) } } return null diff --git a/packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt b/packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt index f36b2aa35fbc..0a0451b8aa67 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt @@ -96,7 +96,26 @@ internal class SharedPreferencesTest { fun testSetAndGetStringListWithDataStore() { val plugin = pluginSetup(dataStoreOptions) plugin.setEncodedStringList(listKey, testList, dataStoreOptions) - Assert.assertEquals(plugin.getStringList(listKey, dataStoreOptions), testList) + val result = plugin.getStringList(listKey, dataStoreOptions) + Assert.assertEquals(result?.jsonEncodedValue, testList) + } + + @Test + fun testSetAndGetStringListWithDataStoreRedirectsForLegacy() { + val plugin = pluginSetup(dataStoreOptions) + plugin.setDeprecatedStringList(listKey, listOf(""), dataStoreOptions) + val result = plugin.getStringList(listKey, dataStoreOptions) + Assert.assertEquals(result?.jsonEncodedValue, null) + Assert.assertEquals(result?.foundPlatformEncodedValue, true) + } + + @Test + fun testSetAndGetStringListWithDataStoreReportsRawString() { + val plugin = pluginSetup(dataStoreOptions) + plugin.setString(listKey, testString, dataStoreOptions) + val result = plugin.getStringList(listKey, dataStoreOptions) + Assert.assertEquals(result?.jsonEncodedValue, null) + Assert.assertEquals(result?.foundPlatformEncodedValue, false) } @Test @@ -217,7 +236,26 @@ internal class SharedPreferencesTest { fun testSetAndGetStringListWithSharedPreferences() { val plugin = pluginSetup(sharedPreferencesOptions) plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions) - Assert.assertEquals(plugin.getStringList(listKey, sharedPreferencesOptions), testList) + val result = plugin.getStringList(listKey, sharedPreferencesOptions) + Assert.assertEquals(result?.jsonEncodedValue, testList) + } + + @Test + fun testSetAndGetStringListWithSharedPreferencesRedirectsForLegacy() { + val plugin = pluginSetup(sharedPreferencesOptions) + plugin.setDeprecatedStringList(listKey, listOf(""), sharedPreferencesOptions) + val result = plugin.getStringList(listKey, sharedPreferencesOptions) + Assert.assertEquals(result?.jsonEncodedValue, null) + Assert.assertEquals(result?.foundPlatformEncodedValue, true) + } + + @Test + fun testSetAndGetStringListWithSharedPreferencesReportsRawString() { + val plugin = pluginSetup(sharedPreferencesOptions) + plugin.setString(listKey, testString, sharedPreferencesOptions) + val result = plugin.getStringList(listKey, sharedPreferencesOptions) + Assert.assertEquals(result?.jsonEncodedValue, null) + Assert.assertEquals(result?.foundPlatformEncodedValue, false) } @Test diff --git a/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart index 59642058d97b..6a221bfdfab3 100644 --- a/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart @@ -639,6 +639,22 @@ void main() { expect(list?.length, testList.length + 1); }); + testWidgets('getStringList throws type error for String with $backend', + (WidgetTester _) async { + final SharedPreferencesAsyncAndroidOptions options = + getOptions(useDataStore: useDataStore, fileName: 'notDefault'); + final SharedPreferencesAsyncPlatform preferences = getPreferences(); + await clearPreferences(preferences, options); + + await preferences.setString(listKey, testString, options); + + // Internally, List is stored as a String on Android, but that + // implementation detail shouldn't leak to clients; getting the wrong + // type should still throw. + expect(preferences.getStringList(listKey, options), + throwsA(isA())); + }); + testWidgets('getPreferences with $backend', (WidgetTester _) async { final SharedPreferencesAsyncAndroidOptions options = getOptions(useDataStore: useDataStore, fileName: 'notDefault'); diff --git a/packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart b/packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart index 09716d1c2b9c..17336b809bbc 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.7.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // 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 @@ -44,6 +44,41 @@ class SharedPreferencesPigeonOptions { } } +class StringListResult { + StringListResult({ + this.jsonEncodedValue, + required this.foundPlatformEncodedValue, + }); + + /// The JSON-encoded stored value, or null if there isn't one. + /// + /// This will be null if either there is no store value (in which case + /// [foundPlatformEncodedValue] will be false), or if there was a + /// platform-encoded value. + String? jsonEncodedValue; + + /// Whether value using the legacy platform-side encoding was found. + /// + /// If this is true, [jsonEncodedValue] will be null, and the value should be + /// fetched with getPlatformEncodedStringList(...) instead. + bool foundPlatformEncodedValue; + + Object encode() { + return [ + jsonEncodedValue, + foundPlatformEncodedValue, + ]; + } + + static StringListResult decode(Object result) { + result as List; + return StringListResult( + jsonEncodedValue: result[0] as String?, + foundPlatformEncodedValue: result[1]! as bool, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -54,6 +89,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is SharedPreferencesPigeonOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is StringListResult) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -64,6 +102,8 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: return SharedPreferencesPigeonOptions.decode(readValue(buffer)!); + case 130: + return StringListResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -373,8 +413,8 @@ class SharedPreferencesAsyncApi { } } - /// Gets individual List value stored with [key], if any. - Future getStringList( + /// Gets the JSON-encoded List value stored with [key], if any. + Future getStringList( String key, SharedPreferencesPigeonOptions options) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.getStringList$pigeonVar_messageChannelSuffix'; @@ -395,7 +435,7 @@ class SharedPreferencesAsyncApi { details: pigeonVar_replyList[2], ); } else { - return (pigeonVar_replyList[0] as String?); + return (pigeonVar_replyList[0] as StringListResult?); } } diff --git a/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_async_android.dart b/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_async_android.dart index e7c30c114d2f..f9f73d66180b 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_async_android.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_async_android.dart @@ -198,9 +198,13 @@ base class SharedPreferencesAsyncAndroid convertOptionsToPigeonOptions(options); final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); // Request JSON encoded string list. - final String? jsonEncodedStringList = - await _convertKnownExceptions( + final StringListResult? result = + await _convertKnownExceptions( () async => api.getStringList(key, pigeonOptions)); + if (result == null) { + return null; + } + final String? jsonEncodedStringList = result.jsonEncodedValue; if (jsonEncodedStringList != null) { final String jsonEncodedString = jsonEncodedStringList.substring(jsonListPrefix.length); @@ -213,10 +217,16 @@ base class SharedPreferencesAsyncAndroid } } // If no JSON encoded string list exists, check for platform encoded value. - final List? stringList = - await _convertKnownExceptions?>( - () async => api.getPlatformEncodedStringList(key, pigeonOptions)); - return stringList?.cast().toList(); + if (result.foundPlatformEncodedValue) { + final List? stringList = + await _convertKnownExceptions?>( + () async => api.getPlatformEncodedStringList(key, pigeonOptions)); + return stringList?.cast().toList(); + } else { + // A non-null result where foundPlatformEncodedValue is false means there + // was a raw string result, so the client is fetching the wrong type. + throw TypeError(); + } } Future _convertKnownExceptions(Future Function() method) async { diff --git a/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart b/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart index 66680044e325..1e279505ffd6 100644 --- a/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart +++ b/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart @@ -24,6 +24,27 @@ class SharedPreferencesPigeonOptions { bool useDataStore; } +class StringListResult { + StringListResult({ + required this.jsonEncodedValue, + required this.foundPlatformEncodedValue, + }); + + /// The JSON-encoded stored value, or null if something else was found, in + /// which case [foundPlatformEncodedValue] will indicate its type. + String? jsonEncodedValue; + + /// Whether value using the legacy platform-side encoding was found. + /// + /// This value is only meaningful if [jsonEncodedValue] is null. + /// - If true, the value should be fetched with + /// getPlatformEncodedStringList(...) instead. + /// - If false, an unexpected string (one without any encoding prefix) was + /// found. This will happen if a client uses getStringList with a key that + /// was used with setString. + bool foundPlatformEncodedValue; +} + @HostApi(dartHostTestHandler: 'TestSharedPreferencesAsyncApi') abstract class SharedPreferencesAsyncApi { /// Adds property to shared preferences data set of type bool. @@ -107,9 +128,9 @@ abstract class SharedPreferencesAsyncApi { SharedPreferencesPigeonOptions options, ); - /// Gets individual List value stored with [key], if any. + /// Gets the JSON-encoded List value stored with [key], if any. @TaskQueue(type: TaskQueueType.serialBackgroundThread) - String? getStringList( + StringListResult? getStringList( String key, SharedPreferencesPigeonOptions options, ); diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index bda1bd0c430a..3e1404bd2813 100644 --- a/packages/shared_preferences/shared_preferences_android/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_android description: Android implementation of the shared_preferences plugin repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.4.3 +version: 2.4.4 environment: sdk: ^3.5.0 diff --git a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_async_test.dart b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_async_test.dart index 404c0439a9dd..26e7dd76b5e8 100755 --- a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_async_test.dart +++ b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_async_test.dart @@ -298,9 +298,14 @@ class _FakeSharedPreferencesApi implements SharedPreferencesAsyncApi { } @override - Future getStringList( + Future getStringList( String key, SharedPreferencesPigeonOptions options) async { - return items[key] as String?; + final Object? value = items[key]; + return value == null + ? null + : StringListResult( + jsonEncodedValue: value as String?, + foundPlatformEncodedValue: false); } @override From 2354e50cb08375ddcde059aeb001491dcaf874e1 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 27 Jan 2025 16:00:14 -0500 Subject: [PATCH 2/2] Review comments --- .../sharedpreferences/MessagesAsync.g.kt | 51 ++++++++++++------- .../SharedPreferencesPlugin.kt | 12 ++--- .../SharedPreferencesTest.kt | 14 ++--- .../lib/src/messages_async.g.dart | 43 ++++++++++------ .../src/shared_preferences_async_android.dart | 40 +++++++-------- .../pigeons/messages_async.dart | 32 +++++++----- .../test/shared_preferences_async_test.dart | 2 +- 7 files changed, 113 insertions(+), 81 deletions(-) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt index f033428022f0..39a19ed68433 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/MessagesAsync.g.kt @@ -43,6 +43,22 @@ class SharedPreferencesError( val details: Any? = null ) : Throwable() +/** Possible types found during a getStringList call. */ +enum class StringListLookupResultType(val raw: Int) { + /** A deprecated platform-side encoding string list. */ + PLATFORM_ENCODED(0), + /** A JSON-encoded string list. */ + JSON_ENCODED(1), + /** A string that doesn't have the expected encoding prefix. */ + UNEXPECTED_STRING(2); + + companion object { + fun ofRaw(raw: Int): StringListLookupResultType? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class SharedPreferencesPigeonOptions(val fileName: String? = null, val useDataStore: Boolean) { companion object { @@ -63,33 +79,23 @@ data class SharedPreferencesPigeonOptions(val fileName: String? = null, val useD /** Generated class from Pigeon that represents data sent in messages. */ data class StringListResult( - /** - * The JSON-encoded stored value, or null if there isn't one. - * - * This will be null if either there is no store value (in which case - * [foundPlatformEncodedValue] will be false), or if there was a platform-encoded value. - */ + /** The JSON-encoded stored value, or null if something else was found. */ val jsonEncodedValue: String? = null, - /** - * Whether value using the legacy platform-side encoding was found. - * - * If this is true, [jsonEncodedValue] will be null, and the value should be fetched with - * getPlatformEncodedStringList(...) instead. - */ - val foundPlatformEncodedValue: Boolean + /** The type of value found. */ + val type: StringListLookupResultType ) { companion object { fun fromList(pigeonVar_list: List): StringListResult { val jsonEncodedValue = pigeonVar_list[0] as String? - val foundPlatformEncodedValue = pigeonVar_list[1] as Boolean - return StringListResult(jsonEncodedValue, foundPlatformEncodedValue) + val type = pigeonVar_list[1] as StringListLookupResultType + return StringListResult(jsonEncodedValue, type) } } fun toList(): List { return listOf( jsonEncodedValue, - foundPlatformEncodedValue, + type, ) } } @@ -98,11 +104,14 @@ private open class MessagesAsyncPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { StringListLookupResultType.ofRaw(it.toInt()) } + } + 130.toByte() -> { return (readValue(buffer) as? List)?.let { SharedPreferencesPigeonOptions.fromList(it) } } - 130.toByte() -> { + 131.toByte() -> { return (readValue(buffer) as? List)?.let { StringListResult.fromList(it) } } else -> super.readValueOfType(type, buffer) @@ -111,12 +120,16 @@ private open class MessagesAsyncPigeonCodec : StandardMessageCodec() { override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { - is SharedPreferencesPigeonOptions -> { + is StringListLookupResultType -> { stream.write(129) + writeValue(stream, value.raw) + } + is SharedPreferencesPigeonOptions -> { + stream.write(130) writeValue(stream, value.toList()) } is StringListResult -> { - stream.write(130) + stream.write(131) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt index 98a125930224..32b628a91c0b 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt @@ -212,11 +212,11 @@ class SharedPreferencesPlugin() : FlutterPlugin, SharedPreferencesAsyncApi { // The JSON-encoded lists use an extended prefix to distinguish them from // lists that using listEncoder. return if (stringValue.startsWith(JSON_LIST_PREFIX)) { - StringListResult(stringValue, foundPlatformEncodedValue = false) + StringListResult(stringValue, StringListLookupResultType.JSON_ENCODED) } else if (stringValue.startsWith(LIST_PREFIX)) { - StringListResult(null, foundPlatformEncodedValue = true) + StringListResult(null, StringListLookupResultType.PLATFORM_ENCODED) } else { - StringListResult(null, foundPlatformEncodedValue = false) + StringListResult(null, StringListLookupResultType.UNEXPECTED_STRING) } } return null @@ -425,11 +425,11 @@ class SharedPreferencesBackend( // The JSON-encoded lists use an extended prefix to distinguish them from // lists that using listEncoder. return if (value!!.startsWith(JSON_LIST_PREFIX)) { - StringListResult(value, foundPlatformEncodedValue = false) + StringListResult(value, StringListLookupResultType.JSON_ENCODED) } else if (value.startsWith(LIST_PREFIX)) { - StringListResult(null, foundPlatformEncodedValue = true) + StringListResult(null, StringListLookupResultType.PLATFORM_ENCODED) } else { - StringListResult(null, foundPlatformEncodedValue = false) + StringListResult(null, StringListLookupResultType.UNEXPECTED_STRING) } } return null diff --git a/packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt b/packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt index 0a0451b8aa67..15c47eb4051e 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt +++ b/packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt @@ -98,15 +98,16 @@ internal class SharedPreferencesTest { plugin.setEncodedStringList(listKey, testList, dataStoreOptions) val result = plugin.getStringList(listKey, dataStoreOptions) Assert.assertEquals(result?.jsonEncodedValue, testList) + Assert.assertEquals(result?.type, StringListLookupResultType.JSON_ENCODED) } @Test - fun testSetAndGetStringListWithDataStoreRedirectsForLegacy() { + fun testSetAndGetStringListWithDataStoreRedirectsForPlatformEncoded() { val plugin = pluginSetup(dataStoreOptions) plugin.setDeprecatedStringList(listKey, listOf(""), dataStoreOptions) val result = plugin.getStringList(listKey, dataStoreOptions) Assert.assertEquals(result?.jsonEncodedValue, null) - Assert.assertEquals(result?.foundPlatformEncodedValue, true) + Assert.assertEquals(result?.type, StringListLookupResultType.PLATFORM_ENCODED) } @Test @@ -115,7 +116,7 @@ internal class SharedPreferencesTest { plugin.setString(listKey, testString, dataStoreOptions) val result = plugin.getStringList(listKey, dataStoreOptions) Assert.assertEquals(result?.jsonEncodedValue, null) - Assert.assertEquals(result?.foundPlatformEncodedValue, false) + Assert.assertEquals(result?.type, StringListLookupResultType.UNEXPECTED_STRING) } @Test @@ -238,15 +239,16 @@ internal class SharedPreferencesTest { plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions) val result = plugin.getStringList(listKey, sharedPreferencesOptions) Assert.assertEquals(result?.jsonEncodedValue, testList) + Assert.assertEquals(result?.type, StringListLookupResultType.JSON_ENCODED) } @Test - fun testSetAndGetStringListWithSharedPreferencesRedirectsForLegacy() { + fun testSetAndGetStringListWithSharedPreferencesRedirectsForPlatformEncoded() { val plugin = pluginSetup(sharedPreferencesOptions) plugin.setDeprecatedStringList(listKey, listOf(""), sharedPreferencesOptions) val result = plugin.getStringList(listKey, sharedPreferencesOptions) Assert.assertEquals(result?.jsonEncodedValue, null) - Assert.assertEquals(result?.foundPlatformEncodedValue, true) + Assert.assertEquals(result?.type, StringListLookupResultType.PLATFORM_ENCODED) } @Test @@ -255,7 +257,7 @@ internal class SharedPreferencesTest { plugin.setString(listKey, testString, sharedPreferencesOptions) val result = plugin.getStringList(listKey, sharedPreferencesOptions) Assert.assertEquals(result?.jsonEncodedValue, null) - Assert.assertEquals(result?.foundPlatformEncodedValue, false) + Assert.assertEquals(result?.type, StringListLookupResultType.UNEXPECTED_STRING) } @Test diff --git a/packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart b/packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart index 17336b809bbc..74cc0f3fd4a6 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/src/messages_async.g.dart @@ -18,6 +18,18 @@ PlatformException _createConnectionError(String channelName) { ); } +/// Possible types found during a getStringList call. +enum StringListLookupResultType { + /// A deprecated platform-side encoding string list. + platformEncoded, + + /// A JSON-encoded string list. + jsonEncoded, + + /// A string that doesn't have the expected encoding prefix. + unexpectedString, +} + class SharedPreferencesPigeonOptions { SharedPreferencesPigeonOptions({ this.fileName, @@ -47,26 +59,19 @@ class SharedPreferencesPigeonOptions { class StringListResult { StringListResult({ this.jsonEncodedValue, - required this.foundPlatformEncodedValue, + required this.type, }); - /// The JSON-encoded stored value, or null if there isn't one. - /// - /// This will be null if either there is no store value (in which case - /// [foundPlatformEncodedValue] will be false), or if there was a - /// platform-encoded value. + /// The JSON-encoded stored value, or null if something else was found. String? jsonEncodedValue; - /// Whether value using the legacy platform-side encoding was found. - /// - /// If this is true, [jsonEncodedValue] will be null, and the value should be - /// fetched with getPlatformEncodedStringList(...) instead. - bool foundPlatformEncodedValue; + /// The type of value found. + StringListLookupResultType type; Object encode() { return [ jsonEncodedValue, - foundPlatformEncodedValue, + type, ]; } @@ -74,7 +79,7 @@ class StringListResult { result as List; return StringListResult( jsonEncodedValue: result[0] as String?, - foundPlatformEncodedValue: result[1]! as bool, + type: result[1]! as StringListLookupResultType, ); } } @@ -86,11 +91,14 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is SharedPreferencesPigeonOptions) { + } else if (value is StringListLookupResultType) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is SharedPreferencesPigeonOptions) { + buffer.putUint8(130); writeValue(buffer, value.encode()); } else if (value is StringListResult) { - buffer.putUint8(130); + buffer.putUint8(131); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -101,8 +109,11 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - return SharedPreferencesPigeonOptions.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : StringListLookupResultType.values[value]; case 130: + return SharedPreferencesPigeonOptions.decode(readValue(buffer)!); + case 131: return StringListResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_async_android.dart b/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_async_android.dart index f9f73d66180b..4cda4bbdffbb 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_async_android.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_async_android.dart @@ -204,28 +204,26 @@ base class SharedPreferencesAsyncAndroid if (result == null) { return null; } - final String? jsonEncodedStringList = result.jsonEncodedValue; - if (jsonEncodedStringList != null) { - final String jsonEncodedString = - jsonEncodedStringList.substring(jsonListPrefix.length); - try { - final List decodedList = - (jsonDecode(jsonEncodedString) as List).cast(); - return decodedList; - } catch (e) { + switch (result.type) { + case StringListLookupResultType.jsonEncoded: + // Force-unwrap is safe because a value is always set for this type. + final String jsonEncodedStringList = result.jsonEncodedValue!; + final String jsonEncodedString = + jsonEncodedStringList.substring(jsonListPrefix.length); + try { + final List decodedList = + (jsonDecode(jsonEncodedString) as List).cast(); + return decodedList; + } catch (e) { + throw TypeError(); + } + case StringListLookupResultType.platformEncoded: + final List? stringList = + await _convertKnownExceptions?>(() async => + api.getPlatformEncodedStringList(key, pigeonOptions)); + return stringList?.cast().toList(); + case StringListLookupResultType.unexpectedString: throw TypeError(); - } - } - // If no JSON encoded string list exists, check for platform encoded value. - if (result.foundPlatformEncodedValue) { - final List? stringList = - await _convertKnownExceptions?>( - () async => api.getPlatformEncodedStringList(key, pigeonOptions)); - return stringList?.cast().toList(); - } else { - // A non-null result where foundPlatformEncodedValue is false means there - // was a raw string result, so the client is fetching the wrong type. - throw TypeError(); } } diff --git a/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart b/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart index 1e279505ffd6..80a9fc207e2b 100644 --- a/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart +++ b/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart @@ -15,6 +15,22 @@ import 'package:pigeon/pigeon.dart'; dartOut: 'lib/src/messages_async.g.dart', copyrightHeader: 'pigeons/copyright.txt', )) + +/// Possible types found during a getStringList call. +enum StringListLookupResultType { + /// A deprecated platform-side encoding string list. + platformEncoded, + + /// A JSON-encoded string list. + jsonEncoded, + + /// A string that doesn't have the expected encoding prefix. + unexpectedString, + + // There is no type for non-string values, as those will throw an exception + // on the native side, so don't need a return value. +} + class SharedPreferencesPigeonOptions { SharedPreferencesPigeonOptions({ this.fileName, @@ -27,22 +43,14 @@ class SharedPreferencesPigeonOptions { class StringListResult { StringListResult({ required this.jsonEncodedValue, - required this.foundPlatformEncodedValue, + required this.type, }); - /// The JSON-encoded stored value, or null if something else was found, in - /// which case [foundPlatformEncodedValue] will indicate its type. + /// The JSON-encoded stored value, or null if something else was found. String? jsonEncodedValue; - /// Whether value using the legacy platform-side encoding was found. - /// - /// This value is only meaningful if [jsonEncodedValue] is null. - /// - If true, the value should be fetched with - /// getPlatformEncodedStringList(...) instead. - /// - If false, an unexpected string (one without any encoding prefix) was - /// found. This will happen if a client uses getStringList with a key that - /// was used with setString. - bool foundPlatformEncodedValue; + /// The type of value found. + StringListLookupResultType type; } @HostApi(dartHostTestHandler: 'TestSharedPreferencesAsyncApi') diff --git a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_async_test.dart b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_async_test.dart index 26e7dd76b5e8..bf5d9b873beb 100755 --- a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_async_test.dart +++ b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_async_test.dart @@ -305,7 +305,7 @@ class _FakeSharedPreferencesApi implements SharedPreferencesAsyncApi { ? null : StringListResult( jsonEncodedValue: value as String?, - foundPlatformEncodedValue: false); + type: StringListLookupResultType.jsonEncoded); } @override