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..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 @@ -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") @@ -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 { @@ -61,22 +77,59 @@ 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 something else was found. */ + val jsonEncodedValue: String? = null, + /** The type of value found. */ + val type: StringListLookupResultType +) { + companion object { + fun fromList(pigeonVar_list: List): StringListResult { + val jsonEncodedValue = pigeonVar_list[0] as String? + val type = pigeonVar_list[1] as StringListLookupResultType + return StringListResult(jsonEncodedValue, type) + } + } + + fun toList(): List { + return listOf( + jsonEncodedValue, + type, + ) + } +} + 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) } } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { StringListResult.fromList(it) } + } else -> super.readValueOfType(type, buffer) } } 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(131) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) @@ -119,8 +172,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..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 @@ -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, StringListLookupResultType.JSON_ENCODED) + } else if (stringValue.startsWith(LIST_PREFIX)) { + StringListResult(null, StringListLookupResultType.PLATFORM_ENCODED) + } else { + StringListResult(null, StringListLookupResultType.UNEXPECTED_STRING) } } 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, StringListLookupResultType.JSON_ENCODED) + } else if (value.startsWith(LIST_PREFIX)) { + StringListResult(null, StringListLookupResultType.PLATFORM_ENCODED) + } else { + 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 f36b2aa35fbc..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 @@ -96,7 +96,27 @@ 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) + Assert.assertEquals(result?.type, StringListLookupResultType.JSON_ENCODED) + } + + @Test + 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?.type, StringListLookupResultType.PLATFORM_ENCODED) + } + + @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?.type, StringListLookupResultType.UNEXPECTED_STRING) } @Test @@ -217,7 +237,27 @@ 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) + Assert.assertEquals(result?.type, StringListLookupResultType.JSON_ENCODED) + } + + @Test + 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?.type, StringListLookupResultType.PLATFORM_ENCODED) + } + + @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?.type, StringListLookupResultType.UNEXPECTED_STRING) } @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..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 @@ -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 @@ -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, @@ -44,6 +56,34 @@ class SharedPreferencesPigeonOptions { } } +class StringListResult { + StringListResult({ + this.jsonEncodedValue, + required this.type, + }); + + /// The JSON-encoded stored value, or null if something else was found. + String? jsonEncodedValue; + + /// The type of value found. + StringListLookupResultType type; + + Object encode() { + return [ + jsonEncodedValue, + type, + ]; + } + + static StringListResult decode(Object result) { + result as List; + return StringListResult( + jsonEncodedValue: result[0] as String?, + type: result[1]! as StringListLookupResultType, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -51,8 +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(131); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -63,7 +109,12 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: + 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); } @@ -373,8 +424,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 +446,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..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 @@ -198,25 +198,33 @@ 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 (jsonEncodedStringList != null) { - final String jsonEncodedString = - jsonEncodedStringList.substring(jsonListPrefix.length); - try { - final List decodedList = - (jsonDecode(jsonEncodedString) as List).cast(); - return decodedList; - } catch (e) { + if (result == null) { + return null; + } + 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. - final List? stringList = - await _convertKnownExceptions?>( - () async => api.getPlatformEncodedStringList(key, pigeonOptions)); - return stringList?.cast().toList(); } 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..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, @@ -24,6 +40,19 @@ class SharedPreferencesPigeonOptions { bool useDataStore; } +class StringListResult { + StringListResult({ + required this.jsonEncodedValue, + required this.type, + }); + + /// The JSON-encoded stored value, or null if something else was found. + String? jsonEncodedValue; + + /// The type of value found. + StringListLookupResultType type; +} + @HostApi(dartHostTestHandler: 'TestSharedPreferencesAsyncApi') abstract class SharedPreferencesAsyncApi { /// Adds property to shared preferences data set of type bool. @@ -107,9 +136,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..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 @@ -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?, + type: StringListLookupResultType.jsonEncoded); } @override