From f7efc2b67a869ccbb40545e6d8bf5941195dcefa Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Fri, 24 Jan 2025 13:23:33 -0800 Subject: [PATCH] [shared_preferences] update List encode/decode (#8335) Follow up to https://github.com/flutter/packages/pull/8187 Updates Android `List` encoding/decoding to use JSON to avoid potential future breakages from restricted list types. --- .../shared_preferences_android/CHANGELOG.md | 4 + .../LegacySharedPreferencesPlugin.java | 27 +- .../plugins/sharedpreferences/Messages.java | 146 +++++--- .../sharedpreferences/MessagesAsync.g.kt | 77 ++++- .../SharedPreferencesPlugin.kt | 98 +++++- .../LegacySharedPreferencesTest.java | 23 +- .../SharedPreferencesTest.kt | 28 +- .../shared_preferences_test.dart | 108 ++++++ .../lib/src/messages.g.dart | 315 +++++++++++------- .../lib/src/messages_async.g.dart | 64 +++- .../lib/src/shared_preferences_android.dart | 36 +- .../src/shared_preferences_async_android.dart | 97 ++++-- .../lib/src/strings.dart | 18 + .../pigeons/messages.dart | 8 +- .../pigeons/messages_async.dart | 21 +- .../shared_preferences_android/pubspec.yaml | 4 +- .../test/shared_preferences_android_test.dart | 98 ++++-- .../test/shared_preferences_async_test.dart | 25 +- 18 files changed, 894 insertions(+), 303 deletions(-) create mode 100644 packages/shared_preferences/shared_preferences_android/lib/src/strings.dart diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index 197babaebb6c..0e2bd32a07f5 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.3 + +* Migrates `List` value encoding to JSON. + ## 2.4.2 * Bumps gradle-plugin to 2.1.0. diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java index 24bfb5074837..4e9ae85bbc66 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java @@ -25,18 +25,22 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** LegacySharedPreferencesPlugin */ public class LegacySharedPreferencesPlugin implements FlutterPlugin, SharedPreferencesApi { private static final String TAG = "SharedPreferencesPlugin"; private static final String SHARED_PREFERENCES_NAME = "FlutterSharedPreferences"; + // All identifiers must match the SharedPreferencesPlugin.kt file, as well as the strings.dart file. private static final String LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"; + // The symbol `!` was chosen as it cannot be created by the base 64 encoding used with LIST_IDENTIFIER. + private static final String JSON_LIST_IDENTIFIER = LIST_IDENTIFIER + "!"; private static final String BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy"; private static final String DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu"; private SharedPreferences preferences; - private SharedPreferencesListEncoder listEncoder; + private final SharedPreferencesListEncoder listEncoder; public LegacySharedPreferencesPlugin() { this(new ListEncoder()); @@ -100,7 +104,15 @@ public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding bin } @Override - public @NonNull Boolean setStringList(@NonNull String key, @NonNull List value) + public @NonNull Boolean setEncodedStringList(@NonNull String key, @NonNull String value) + throws RuntimeException { + return preferences.edit().putString(key, value).commit(); + } + + // Deprecated, for testing purposes only. + @Deprecated + @Override + public @NonNull Boolean setDeprecatedStringList(@NonNull String key, @NonNull List value) throws RuntimeException { return preferences.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)).commit(); } @@ -131,14 +143,13 @@ public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding bin // Gets all shared preferences, filtered to only those set with the given prefix. // Optionally filtered also to only those items in the optional [allowList]. - @SuppressWarnings("unchecked") private @NonNull Map getAllPrefs( @NonNull String prefix, @Nullable Set allowList) throws RuntimeException { Map allPrefs = preferences.getAll(); Map filteredPrefs = new HashMap<>(); for (String key : allPrefs.keySet()) { if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { - filteredPrefs.put(key, transformPref(key, allPrefs.get(key))); + filteredPrefs.put(key, transformPref(key, Objects.requireNonNull(allPrefs.get(key)))); } } @@ -149,7 +160,13 @@ private Object transformPref(@NonNull String key, @NonNull Object value) { if (value instanceof String) { String stringValue = (String) value; if (stringValue.startsWith(LIST_IDENTIFIER)) { - return listEncoder.decode(stringValue.substring(LIST_IDENTIFIER.length())); + // The JSON-encoded lists use an extended prefix to distinguish them from + // lists that are encoded on the platform. + if (stringValue.startsWith(JSON_LIST_IDENTIFIER)) { + return value; + } else { + return listEncoder.decode(stringValue.substring(LIST_IDENTIFIER.length())); + } } else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) { // TODO (tarrinneal): Remove all BigInt code. // https://github.com/flutter/flutter/issues/124420 diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/Messages.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/Messages.java index 4041ad9aa6f2..8ea865ca5e06 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/Messages.java +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/Messages.java @@ -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 (v16.0.4), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.sharedpreferences; @@ -13,6 +13,8 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MessageCodec; import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -39,7 +41,7 @@ public FlutterError(@NonNull String code, @Nullable String message, @Nullable Ob @NonNull protected static ArrayList wrapError(@NonNull Throwable exception) { - ArrayList errorList = new ArrayList(3); + ArrayList errorList = new ArrayList<>(3); if (exception instanceof FlutterError) { FlutterError error = (FlutterError) exception; errorList.add(error.code); @@ -53,6 +55,28 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { } return errorList; } + + private static class PigeonCodec extends StandardMessageCodec { + public static final PigeonCodec INSTANCE = new PigeonCodec(); + + private PigeonCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + { + super.writeValue(stream, value); + } + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface SharedPreferencesApi { /** Removes property from shared preferences data set. */ @@ -72,7 +96,14 @@ public interface SharedPreferencesApi { Boolean setDouble(@NonNull String key, @NonNull Double value); /** Adds property to shared preferences data set of type List. */ @NonNull - Boolean setStringList(@NonNull String key, @NonNull List value); + Boolean setEncodedStringList(@NonNull String key, @NonNull String value); + /** + * Adds property to shared preferences data set of type List. + * + *

Deprecated, this is only here for testing purposes. + */ + @NonNull + Boolean setDeprecatedStringList(@NonNull String key, @NonNull List value); /** Removes all properties from shared preferences data set with matching prefix. */ @NonNull Boolean clear(@NonNull String prefix, @Nullable List allowList); @@ -82,7 +113,7 @@ public interface SharedPreferencesApi { /** The codec used by SharedPreferencesApi. */ static @NonNull MessageCodec getCodec() { - return new StandardMessageCodec(); + return PigeonCodec.INSTANCE; } /** * Sets up an instance of `SharedPreferencesApi` to handle messages through the @@ -90,26 +121,34 @@ public interface SharedPreferencesApi { */ static void setUp( @NonNull BinaryMessenger binaryMessenger, @Nullable SharedPreferencesApi api) { + setUp(binaryMessenger, "", api); + } + + static void setUp( + @NonNull BinaryMessenger binaryMessenger, + @NonNull String messageChannelSuffix, + @Nullable SharedPreferencesApi api) { + messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; { BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.remove", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.remove" + + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { channel.setMessageHandler( (message, reply) -> { - ArrayList wrapped = new ArrayList(); + ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; String keyArg = (String) args.get(0); try { Boolean output = api.remove(keyArg); wrapped.add(0, output); } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; + wrapped = wrapError(exception); } reply.reply(wrapped); }); @@ -122,13 +161,14 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setBool", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setBool" + + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { channel.setMessageHandler( (message, reply) -> { - ArrayList wrapped = new ArrayList(); + ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; String keyArg = (String) args.get(0); Boolean valueArg = (Boolean) args.get(1); @@ -136,8 +176,7 @@ static void setUp( Boolean output = api.setBool(keyArg, valueArg); wrapped.add(0, output); } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; + wrapped = wrapError(exception); } reply.reply(wrapped); }); @@ -150,13 +189,14 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setString", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setString" + + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { channel.setMessageHandler( (message, reply) -> { - ArrayList wrapped = new ArrayList(); + ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; String keyArg = (String) args.get(0); String valueArg = (String) args.get(1); @@ -164,8 +204,7 @@ static void setUp( Boolean output = api.setString(keyArg, valueArg); wrapped.add(0, output); } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; + wrapped = wrapError(exception); } reply.reply(wrapped); }); @@ -178,23 +217,22 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setInt", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setInt" + + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { channel.setMessageHandler( (message, reply) -> { - ArrayList wrapped = new ArrayList(); + ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; String keyArg = (String) args.get(0); - Number valueArg = (Number) args.get(1); + Long valueArg = (Long) args.get(1); try { - Boolean output = - api.setInt(keyArg, (valueArg == null) ? null : valueArg.longValue()); + Boolean output = api.setInt(keyArg, valueArg); wrapped.add(0, output); } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; + wrapped = wrapError(exception); } reply.reply(wrapped); }); @@ -207,13 +245,14 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setDouble", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setDouble" + + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { channel.setMessageHandler( (message, reply) -> { - ArrayList wrapped = new ArrayList(); + ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; String keyArg = (String) args.get(0); Double valueArg = (Double) args.get(1); @@ -221,8 +260,35 @@ static void setUp( Boolean output = api.setDouble(keyArg, valueArg); wrapped.add(0, output); } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setEncodedStringList" + + messageChannelSuffix, + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + String keyArg = (String) args.get(0); + String valueArg = (String) args.get(1); + try { + Boolean output = api.setEncodedStringList(keyArg, valueArg); + wrapped.add(0, output); + } catch (Throwable exception) { + wrapped = wrapError(exception); } reply.reply(wrapped); }); @@ -235,22 +301,22 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setStringList", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setDeprecatedStringList" + + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { channel.setMessageHandler( (message, reply) -> { - ArrayList wrapped = new ArrayList(); + ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; String keyArg = (String) args.get(0); List valueArg = (List) args.get(1); try { - Boolean output = api.setStringList(keyArg, valueArg); + Boolean output = api.setDeprecatedStringList(keyArg, valueArg); wrapped.add(0, output); } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; + wrapped = wrapError(exception); } reply.reply(wrapped); }); @@ -263,13 +329,14 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.clear", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.clear" + + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { channel.setMessageHandler( (message, reply) -> { - ArrayList wrapped = new ArrayList(); + ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; String prefixArg = (String) args.get(0); List allowListArg = (List) args.get(1); @@ -277,8 +344,7 @@ static void setUp( Boolean output = api.clear(prefixArg, allowListArg); wrapped.add(0, output); } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; + wrapped = wrapError(exception); } reply.reply(wrapped); }); @@ -291,13 +357,14 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.getAll", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.getAll" + + messageChannelSuffix, getCodec(), taskQueue); if (api != null) { channel.setMessageHandler( (message, reply) -> { - ArrayList wrapped = new ArrayList(); + ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; String prefixArg = (String) args.get(0); List allowListArg = (List) args.get(1); @@ -305,8 +372,7 @@ static void setUp( Map output = api.getAll(prefixArg, allowListArg); wrapped.add(0, output); } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; + wrapped = wrapError(exception); } reply.reply(wrapped); }); 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 6855bad260b9..44c37bad67da 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.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -95,7 +95,17 @@ interface SharedPreferencesAsyncApi { /** Adds property to shared preferences data set of type double. */ fun setDouble(key: String, value: Double, options: SharedPreferencesPigeonOptions) /** Adds property to shared preferences data set of type List. */ - fun setStringList(key: String, value: List, options: SharedPreferencesPigeonOptions) + fun setEncodedStringList(key: String, value: String, options: SharedPreferencesPigeonOptions) + /** + * Adds property to shared preferences data set of type List. + * + * Deprecated, this is only here for testing purposes. + */ + fun setDeprecatedStringList( + key: String, + value: List, + options: SharedPreferencesPigeonOptions + ) /** Gets individual String value stored with [key], if any. */ fun getString(key: String, options: SharedPreferencesPigeonOptions): String? /** Gets individual void value stored with [key], if any. */ @@ -105,7 +115,12 @@ interface SharedPreferencesAsyncApi { /** Gets individual int value stored with [key], if any. */ fun getInt(key: String, options: SharedPreferencesPigeonOptions): Long? /** Gets individual List value stored with [key], if any. */ - fun getStringList(key: String, options: SharedPreferencesPigeonOptions): List? + fun getPlatformEncodedStringList( + key: String, + options: SharedPreferencesPigeonOptions + ): List? + /** Gets individual List value stored with [key], if any. */ + fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? /** 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. */ @@ -241,7 +256,34 @@ interface SharedPreferencesAsyncApi { val channel = BasicMessageChannel( binaryMessenger, - "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.setStringList$separatedMessageChannelSuffix", + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.setEncodedStringList$separatedMessageChannelSuffix", + codec, + taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val keyArg = args[0] as String + val valueArg = args[1] as String + val optionsArg = args[2] as SharedPreferencesPigeonOptions + val wrapped: List = + try { + api.setEncodedStringList(keyArg, valueArg, optionsArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val taskQueue = binaryMessenger.makeBackgroundTaskQueue() + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.setDeprecatedStringList$separatedMessageChannelSuffix", codec, taskQueue) if (api != null) { @@ -252,7 +294,7 @@ interface SharedPreferencesAsyncApi { val optionsArg = args[2] as SharedPreferencesPigeonOptions val wrapped: List = try { - api.setStringList(keyArg, valueArg, optionsArg) + api.setDeprecatedStringList(keyArg, valueArg, optionsArg) listOf(null) } catch (exception: Throwable) { wrapError(exception) @@ -363,6 +405,31 @@ interface SharedPreferencesAsyncApi { channel.setMessageHandler(null) } } + run { + val taskQueue = binaryMessenger.makeBackgroundTaskQueue() + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.getPlatformEncodedStringList$separatedMessageChannelSuffix", + codec, + taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val keyArg = args[0] as String + val optionsArg = args[1] as SharedPreferencesPigeonOptions + val wrapped: List = + try { + listOf(api.getPlatformEncodedStringList(keyArg, optionsArg)) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val taskQueue = binaryMessenger.makeBackgroundTaskQueue() val channel = 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 e0ca35b258cf..4587c68bab6f 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 @@ -30,7 +30,11 @@ import kotlinx.coroutines.runBlocking const val TAG = "SharedPreferencesPlugin" const val SHARED_PREFERENCES_NAME = "FlutterSharedPreferences" +// All identifiers must match the LegacySharedPreferencesPlugin.java file, as well as the +// strings.dart file. const val LIST_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" +// The symbol `!` was chosen as it cannot be created by the base 64 encoding used with LIST_PREFIX. +const val JSON_LIST_PREFIX = LIST_PREFIX + "!" const val DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu" private val Context.sharedPreferencesDataStore: DataStore by @@ -103,8 +107,18 @@ class SharedPreferencesPlugin() : FlutterPlugin, SharedPreferencesAsyncApi { } } - /** Adds property to data store of type List. */ - override fun setStringList( + /** Adds property to data store of type List as encoded String. */ + override fun setEncodedStringList( + key: String, + value: String, + options: SharedPreferencesPigeonOptions + ) { + return runBlocking { dataStoreSetString(key, value) } + } + + /** Deprecated, for testing purposes only. Adds property to data store of type List. */ + @Deprecated("This is just for testing, use `setEncodedStringList`") + override fun setDeprecatedStringList( key: String, value: List, options: SharedPreferencesPigeonOptions @@ -189,9 +203,33 @@ class SharedPreferencesPlugin() : FlutterPlugin, SharedPreferencesAsyncApi { } /** Gets StringList at [key] from data store. */ - override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): List? { - val value: List<*>? = transformPref(getString(key, options) as Any?, listEncoder) as List<*>? - return value?.filterIsInstance() + override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? { + 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 null + } + + /** Gets StringList at [key] from data store. */ + override fun getPlatformEncodedStringList( + key: String, + options: SharedPreferencesPigeonOptions + ): List? { + 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) && stringValue.startsWith(LIST_PREFIX)) { + val value: List<*>? = transformPref(stringValue, listEncoder) as List<*>? + return value?.filterIsInstance() + } + } + return null } /** Gets all properties from data store. */ @@ -278,7 +316,17 @@ class SharedPreferencesBackend( } /** Adds property to data store of type List. */ - override fun setStringList( + override fun setEncodedStringList( + key: String, + value: String, + options: SharedPreferencesPigeonOptions + ) { + return createSharedPreferences(options).edit().putString(key, value).apply() + } + + /** Adds property to data store of type List. */ + @Deprecated("This is just for testing, use `setEncodedStringList`") + override fun setDeprecatedStringList( key: String, value: List, options: SharedPreferencesPigeonOptions @@ -339,6 +387,7 @@ class SharedPreferencesBackend( null } } + /** Gets double at [key] from data store. */ override fun getDouble(key: String, options: SharedPreferencesPigeonOptions): Double? { val preferences = createSharedPreferences(options) @@ -348,7 +397,6 @@ class SharedPreferencesBackend( null } } - /** Gets String at [key] from data store. */ override fun getString(key: String, options: SharedPreferencesPigeonOptions): String? { val preferences = createSharedPreferences(options) @@ -360,14 +408,30 @@ class SharedPreferencesBackend( } /** Gets StringList at [key] from data store. */ - override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): List? { + override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? { val preferences = createSharedPreferences(options) - return if (preferences.contains(key)) { - (transformPref(preferences.getString(key, ""), listEncoder) as List<*>?)?.filterIsInstance< - String>() - } else { - null + if (preferences.contains(key)) { + val value = preferences.getString(key, "") + if (value!!.startsWith(JSON_LIST_PREFIX)) { + return value + } } + return null + } + + override fun getPlatformEncodedStringList( + key: String, + options: SharedPreferencesPigeonOptions + ): List? { + val preferences = createSharedPreferences(options) + if (preferences.contains(key)) { + val value = preferences.getString(key, "") + if (value!!.startsWith(LIST_PREFIX) && !value!!.startsWith(JSON_LIST_PREFIX)) { + val transformed = transformPref(preferences.getString(key, ""), listEncoder) + return (transformed as List<*>?)?.filterIsInstance() + } + } + return null } /** Gets all properties from data store. */ @@ -401,7 +465,13 @@ internal fun preferencesFilter(key: String, value: Any?, allowList: Set? internal fun transformPref(value: Any?, listEncoder: SharedPreferencesListEncoder): Any? { if (value is String) { if (value.startsWith(LIST_PREFIX)) { - return listEncoder.decode(value.substring(LIST_PREFIX.length)) + // The JSON-encoded lists use an extended prefix to distinguish them from + // lists that are encoded on the platform. + return if (value.startsWith(JSON_LIST_PREFIX)) { + value + } else { + listEncoder.decode(value.substring(LIST_PREFIX.length)) + } } else if (value.startsWith(DOUBLE_PREFIX)) { return value.substring(DOUBLE_PREFIX.length).toDouble() } diff --git a/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesTest.java b/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesTest.java index 08bbf2fe219f..2ae67ec843d8 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesTest.java +++ b/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesTest.java @@ -13,6 +13,7 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import java.util.Arrays; @@ -54,17 +55,17 @@ public void before() { data.put("Language", "Java"); data.put("Counter", 0L); data.put("Pie", 3.14); - data.put("Names", Arrays.asList("Flutter", "Dart")); + data.put("Names", Arrays.asList("Flutter", "Dart").toString()); data.put("NewToFlutter", false); data.put("flutter.Language", "Java"); data.put("flutter.Counter", 0L); data.put("flutter.Pie", 3.14); - data.put("flutter.Names", Arrays.asList("Flutter", "Dart")); + data.put("flutter.Names", Arrays.asList("Flutter", "Dart").toString()); data.put("flutter.NewToFlutter", false); data.put("prefix.Language", "Java"); data.put("prefix.Counter", 0L); data.put("prefix.Pie", 3.14); - data.put("prefix.Names", Arrays.asList("Flutter", "Dart")); + data.put("prefix.Names", Arrays.asList("Flutter", "Dart").toString()); data.put("prefix.NewToFlutter", false); } @@ -80,7 +81,7 @@ public void getAll() { assertEquals(flutterData.get("flutter.Language"), "Java"); assertEquals(flutterData.get("flutter.Counter"), 0L); assertEquals(flutterData.get("flutter.Pie"), 3.14); - assertEquals(flutterData.get("flutter.Names"), Arrays.asList("Flutter", "Dart")); + assertEquals(flutterData.get("flutter.Names"), Arrays.asList("Flutter", "Dart").toString()); assertEquals(flutterData.get("flutter.NewToFlutter"), false); Map allData = plugin.getAll("", null); @@ -142,10 +143,10 @@ public void setDouble() { } @Test - public void setStringList() { + public void setEncodedStringListSetsAndGetsString() { final String key = "Names"; - final List value = Arrays.asList("Flutter", "Dart"); - plugin.setStringList(key, value); + final String value = Arrays.asList("Flutter", "Dart").toString(); + plugin.setEncodedStringList(key, value); Map flutterData = plugin.getAll("", null); assertEquals(flutterData.get(key), value); } @@ -206,17 +207,17 @@ private void addData() { plugin.setString("Language", "Java"); plugin.setInt("Counter", 0L); plugin.setDouble("Pie", 3.14); - plugin.setStringList("Names", Arrays.asList("Flutter", "Dart")); + plugin.setEncodedStringList("Names", Arrays.asList("Flutter", "Dart").toString()); plugin.setBool("NewToFlutter", false); plugin.setString("flutter.Language", "Java"); plugin.setInt("flutter.Counter", 0L); plugin.setDouble("flutter.Pie", 3.14); - plugin.setStringList("flutter.Names", Arrays.asList("Flutter", "Dart")); + plugin.setEncodedStringList("flutter.Names", Arrays.asList("Flutter", "Dart").toString()); plugin.setBool("flutter.NewToFlutter", false); plugin.setString("prefix.Language", "Java"); plugin.setInt("prefix.Counter", 0L); plugin.setDouble("prefix.Pie", 3.14); - plugin.setStringList("prefix.Names", Arrays.asList("Flutter", "Dart")); + plugin.setEncodedStringList("prefix.Names", Arrays.asList("Flutter", "Dart").toString()); plugin.setBool("prefix.NewToFlutter", false); } @@ -236,7 +237,7 @@ public static class FakeSharedPreferencesEditor implements SharedPreferences.Edi @Override public @NonNull SharedPreferences.Editor putStringSet( - @NonNull String key, @NonNull Set values) { + @NonNull String key, @Nullable Set values) { sharedPrefData.put(key, values); return this; } 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 1803138bc123..f36b2aa35fbc 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 @@ -41,7 +41,7 @@ internal class SharedPreferencesTest { private val testDouble = 3.14159 - private val testList = listOf("foo", "bar") + private val testList = JSON_LIST_PREFIX + listOf("foo", "bar").toString() private val dataStoreOptions = SharedPreferencesPigeonOptions(useDataStore = true) private val sharedPreferencesOptions = SharedPreferencesPigeonOptions(useDataStore = false) @@ -95,7 +95,7 @@ internal class SharedPreferencesTest { @Test fun testSetAndGetStringListWithDataStore() { val plugin = pluginSetup(dataStoreOptions) - plugin.setStringList(listKey, testList, dataStoreOptions) + plugin.setEncodedStringList(listKey, testList, dataStoreOptions) Assert.assertEquals(plugin.getStringList(listKey, dataStoreOptions), testList) } @@ -106,7 +106,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, dataStoreOptions) plugin.setInt(intKey, testInt, dataStoreOptions) plugin.setDouble(doubleKey, testDouble, dataStoreOptions) - plugin.setStringList(listKey, testList, dataStoreOptions) + plugin.setEncodedStringList(listKey, testList, dataStoreOptions) val keyList = plugin.getKeys(listOf(boolKey, stringKey), dataStoreOptions) Assert.assertEquals(keyList.size, 2) Assert.assertTrue(keyList.contains(stringKey)) @@ -120,7 +120,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, dataStoreOptions) plugin.setInt(intKey, testInt, dataStoreOptions) plugin.setDouble(doubleKey, testDouble, dataStoreOptions) - plugin.setStringList(listKey, testList, dataStoreOptions) + plugin.setEncodedStringList(listKey, testList, dataStoreOptions) plugin.clear(null, dataStoreOptions) @@ -138,7 +138,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, dataStoreOptions) plugin.setInt(intKey, testInt, dataStoreOptions) plugin.setDouble(doubleKey, testDouble, dataStoreOptions) - plugin.setStringList(listKey, testList, dataStoreOptions) + plugin.setEncodedStringList(listKey, testList, dataStoreOptions) val all = plugin.getAll(null, dataStoreOptions) @@ -156,7 +156,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, dataStoreOptions) plugin.setInt(intKey, testInt, dataStoreOptions) plugin.setDouble(doubleKey, testDouble, dataStoreOptions) - plugin.setStringList(listKey, testList, dataStoreOptions) + plugin.setEncodedStringList(listKey, testList, dataStoreOptions) plugin.clear(listOf(boolKey, stringKey), dataStoreOptions) @@ -174,7 +174,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, dataStoreOptions) plugin.setInt(intKey, testInt, dataStoreOptions) plugin.setDouble(doubleKey, testDouble, dataStoreOptions) - plugin.setStringList(listKey, testList, dataStoreOptions) + plugin.setEncodedStringList(listKey, testList, dataStoreOptions) val all = plugin.getAll(listOf(boolKey, stringKey), dataStoreOptions) @@ -216,7 +216,7 @@ internal class SharedPreferencesTest { @Test fun testSetAndGetStringListWithSharedPreferences() { val plugin = pluginSetup(sharedPreferencesOptions) - plugin.setStringList(listKey, testList, sharedPreferencesOptions) + plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions) Assert.assertEquals(plugin.getStringList(listKey, sharedPreferencesOptions), testList) } @@ -227,7 +227,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, sharedPreferencesOptions) plugin.setInt(intKey, testInt, sharedPreferencesOptions) plugin.setDouble(doubleKey, testDouble, sharedPreferencesOptions) - plugin.setStringList(listKey, testList, sharedPreferencesOptions) + plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions) val keyList = plugin.getKeys(listOf(boolKey, stringKey), sharedPreferencesOptions) Assert.assertEquals(keyList.size, 2) Assert.assertTrue(keyList.contains(stringKey)) @@ -241,7 +241,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, sharedPreferencesOptions) plugin.setInt(intKey, testInt, sharedPreferencesOptions) plugin.setDouble(doubleKey, testDouble, sharedPreferencesOptions) - plugin.setStringList(listKey, testList, sharedPreferencesOptions) + plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions) plugin.clear(null, sharedPreferencesOptions) @@ -259,7 +259,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, sharedPreferencesOptions) plugin.setInt(intKey, testInt, sharedPreferencesOptions) plugin.setDouble(doubleKey, testDouble, sharedPreferencesOptions) - plugin.setStringList(listKey, testList, sharedPreferencesOptions) + plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions) val all = plugin.getAll(null, sharedPreferencesOptions) @@ -277,7 +277,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, sharedPreferencesOptions) plugin.setInt(intKey, testInt, sharedPreferencesOptions) plugin.setDouble(doubleKey, testDouble, sharedPreferencesOptions) - plugin.setStringList(listKey, testList, sharedPreferencesOptions) + plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions) plugin.clear(listOf(boolKey, stringKey), sharedPreferencesOptions) @@ -295,7 +295,7 @@ internal class SharedPreferencesTest { plugin.setString(stringKey, testString, sharedPreferencesOptions) plugin.setInt(intKey, testInt, sharedPreferencesOptions) plugin.setDouble(doubleKey, testDouble, sharedPreferencesOptions) - plugin.setStringList(listKey, testList, sharedPreferencesOptions) + plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions) val all = plugin.getAll(listOf(boolKey, stringKey), sharedPreferencesOptions) @@ -342,7 +342,7 @@ internal class SharedPreferencesTest { // Inject the bad pref as a string, as that is how string lists are stored internally. plugin.setString(badListKey, badPref, dataStoreOptions) assertThrows(ClassNotFoundException::class.java) { - plugin.getStringList(badListKey, dataStoreOptions) + plugin.getPlatformEncodedStringList(badListKey, dataStoreOptions) } } } 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 0335b00968b8..59642058d97b 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 @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences_android/shared_preferences_android.dart'; +import 'package:shared_preferences_android/src/messages_async.g.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_platform_interface/types.dart'; @@ -493,6 +494,39 @@ void main() { expect(values[key], null); } }); + + testWidgets( + 'Platform list encoding with getPreferences can be re-added with new encoding without data loss', + (WidgetTester _) async { + await preferences.clearWithParameters( + ClearParameters( + filter: PreferencesFilter(prefix: ''), + ), + ); + await preferences.setValue('String', 'String', allTestValues['String']!); + await preferences.setValue('Bool', 'Bool', allTestValues['Bool']!); + await preferences.setValue('Int', 'Int', allTestValues['Int']!); + await preferences.setValue('Double', 'Double', allTestValues['Double']!); + await (preferences as SharedPreferencesAndroid) + .api + .setDeprecatedStringList( + 'StringList', allTestValues['StringList']! as List); + Map prefs = await preferences.getAllWithParameters( + GetAllParameters( + filter: PreferencesFilter(prefix: ''), + ), + ); + expect(prefs['StringList'], allTestValues['StringList']); + await preferences.setValue( + 'StringList', 'StringList', prefs['StringList']!); + prefs = await preferences.getAllWithParameters( + GetAllParameters( + filter: PreferencesFilter(prefix: ''), + ), + ); + + expect(prefs['StringList'], allTestValues['StringList']); + }); }); const String stringKey = 'testString'; @@ -757,6 +791,80 @@ void main() { expect(await preferences.getDouble(doubleKey, options), testDouble); expect(await preferences.getStringList(listKey, options), testList); }); + + testWidgets( + 'platform list encoding updates to JSON encoding process without data loss with $backend', + (WidgetTester _) async { + final SharedPreferencesAsyncAndroidOptions options = + getOptions(useDataStore: useDataStore, fileName: 'notDefault'); + final SharedPreferencesAsyncAndroid preferences = + getPreferences() as SharedPreferencesAsyncAndroid; + await clearPreferences(preferences, options); + final SharedPreferencesPigeonOptions pigeonOptions = + preferences.convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = + preferences.getApiForBackend(pigeonOptions); + await api.setDeprecatedStringList(listKey, testList, pigeonOptions); + final List? platformEncodedList = + await preferences.getStringList(listKey, options); + expect(platformEncodedList, testList); + await preferences.setStringList(listKey, platformEncodedList!, options); + expect(await preferences.getStringList(listKey, options), testList); + }); + + testWidgets( + 'platform list encoding still functions with getPreferences with $backend', + (WidgetTester _) async { + final SharedPreferencesAsyncAndroidOptions options = + getOptions(useDataStore: useDataStore, fileName: 'notDefault'); + final SharedPreferencesAsyncAndroid preferences = + getPreferences() as SharedPreferencesAsyncAndroid; + await clearPreferences(preferences, options); + await Future.wait(>[ + preferences.setString(stringKey, testString, options), + preferences.setBool(boolKey, testBool, options), + preferences.setInt(intKey, testInt, options), + preferences.setDouble(doubleKey, testDouble, options), + ]); + final SharedPreferencesPigeonOptions pigeonOptions = + preferences.convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = + preferences.getApiForBackend(pigeonOptions); + await api.setDeprecatedStringList(listKey, testList, pigeonOptions); + + final Map prefs = await preferences.getPreferences( + const GetPreferencesParameters(filter: PreferencesFilters()), + options); + expect(prefs[listKey], testList); + }); + + testWidgets( + 'platform list encoding with getPreferences can be re-added with new encoding without data loss with $backend', + (WidgetTester _) async { + final SharedPreferencesAsyncAndroidOptions options = + getOptions(useDataStore: useDataStore, fileName: 'notDefault'); + final SharedPreferencesAsyncAndroid preferences = + getPreferences() as SharedPreferencesAsyncAndroid; + await clearPreferences(preferences, options); + await Future.wait(>[ + preferences.setString(stringKey, testString, options), + preferences.setBool(boolKey, testBool, options), + preferences.setInt(intKey, testInt, options), + preferences.setDouble(doubleKey, testDouble, options), + ]); + final SharedPreferencesPigeonOptions pigeonOptions = + preferences.convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = + preferences.getApiForBackend(pigeonOptions); + await api.setDeprecatedStringList(listKey, testList, pigeonOptions); + + final Map prefs = await preferences.getPreferences( + const GetPreferencesParameters(filter: PreferencesFilters()), + options); + await preferences.setStringList(listKey, + (prefs[listKey]! as List).cast(), options); + expect(await preferences.getStringList(listKey, options), testList); + }); }); } diff --git a/packages/shared_preferences/shared_preferences_android/lib/src/messages.g.dart b/packages/shared_preferences/shared_preferences_android/lib/src/messages.g.dart index 3a0034796d39..b94a30e61fb2 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/src/messages.g.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/src/messages.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 (v16.0.4), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), 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,256 +18,313 @@ PlatformException _createConnectionError(String channelName) { ); } +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + class SharedPreferencesApi { /// Constructor for [SharedPreferencesApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - SharedPreferencesApi({BinaryMessenger? binaryMessenger}) - : __pigeon_binaryMessenger = binaryMessenger; - final BinaryMessenger? __pigeon_binaryMessenger; + SharedPreferencesApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - static const MessageCodec pigeonChannelCodec = - StandardMessageCodec(); + final String pigeonVar_messageChannelSuffix; /// Removes property from shared preferences data set. Future remove(String key) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.remove'; - final BasicMessageChannel __pigeon_channel = + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.remove$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send([key]) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final List? pigeonVar_replyList = + await pigeonVar_channel.send([key]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Adds property to shared preferences data set of type bool. Future setBool(String key, bool value) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setBool'; - final BasicMessageChannel __pigeon_channel = + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setBool$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send([key, value]) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final List? pigeonVar_replyList = + await pigeonVar_channel.send([key, value]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Adds property to shared preferences data set of type String. Future setString(String key, String value) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setString'; - final BasicMessageChannel __pigeon_channel = + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setString$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send([key, value]) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final List? pigeonVar_replyList = + await pigeonVar_channel.send([key, value]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Adds property to shared preferences data set of type int. Future setInt(String key, int value) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setInt'; - final BasicMessageChannel __pigeon_channel = + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setInt$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send([key, value]) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final List? pigeonVar_replyList = + await pigeonVar_channel.send([key, value]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Adds property to shared preferences data set of type double. Future setDouble(String key, double value) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setDouble'; - final BasicMessageChannel __pigeon_channel = + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setDouble$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([key, value]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + /// Adds property to shared preferences data set of type List. + Future setEncodedStringList(String key, String value) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setEncodedStringList$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send([key, value]) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final List? pigeonVar_replyList = + await pigeonVar_channel.send([key, value]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Adds property to shared preferences data set of type List. - Future setStringList(String key, List value) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setStringList'; - final BasicMessageChannel __pigeon_channel = + /// + /// Deprecated, this is only here for testing purposes. + Future setDeprecatedStringList(String key, List value) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.setDeprecatedStringList$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = - await __pigeon_channel.send([key, value]) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + final List? pigeonVar_replyList = + await pigeonVar_channel.send([key, value]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Removes all properties from shared preferences data set with matching prefix. - Future clear(String prefix, List? allowList) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.clear'; - final BasicMessageChannel __pigeon_channel = + Future clear(String prefix, List? allowList) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.clear$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel + final List? pigeonVar_replyList = await pigeonVar_channel .send([prefix, allowList]) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as bool?)!; } } /// Gets all properties from shared preferences data set with matching prefix. - Future> getAll( - String prefix, List? allowList) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.getAll'; - final BasicMessageChannel __pigeon_channel = + Future> getAll( + String prefix, List? allowList) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesApi.getAll$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - __pigeon_channelName, + pigeonVar_channelName, pigeonChannelCodec, - binaryMessenger: __pigeon_binaryMessenger, + binaryMessenger: pigeonVar_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel + final List? pigeonVar_replyList = await pigeonVar_channel .send([prefix, allowList]) as List?; - if (__pigeon_replyList == null) { - throw _createConnectionError(__pigeon_channelName); - } else if (__pigeon_replyList.length > 1) { + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: __pigeon_replyList[0]! as String, - message: __pigeon_replyList[1] as String?, - details: __pigeon_replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (__pigeon_replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as Map?)! - .cast(); + return (pigeonVar_replyList[0] as Map?)! + .cast(); } } } 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 c21b49cca96f..09716d1c2b9c 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.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), 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 @@ -190,10 +190,38 @@ class SharedPreferencesAsyncApi { } /// Adds property to shared preferences data set of type List. - Future setStringList(String key, List value, + Future setEncodedStringList( + String key, String value, SharedPreferencesPigeonOptions options) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.setEncodedStringList$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([key, value, options]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Adds property to shared preferences data set of type List. + /// + /// Deprecated, this is only here for testing purposes. + Future setDeprecatedStringList(String key, List value, SharedPreferencesPigeonOptions options) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.setStringList$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.setDeprecatedStringList$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -320,10 +348,10 @@ class SharedPreferencesAsyncApi { } /// Gets individual List value stored with [key], if any. - Future?> getStringList( + Future?> getPlatformEncodedStringList( String key, SharedPreferencesPigeonOptions options) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.getStringList$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.getPlatformEncodedStringList$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -345,6 +373,32 @@ class SharedPreferencesAsyncApi { } } + /// Gets individual 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'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([key, options]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as String?); + } + } + /// Removes all properties from shared preferences data set with matching prefix. Future clear( List? allowList, SharedPreferencesPigeonOptions options) async { diff --git a/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_android.dart b/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_android.dart index 67cc0835635d..c10a3a6362a0 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_android.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/src/shared_preferences_android.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; @@ -9,6 +11,7 @@ import 'package:shared_preferences_platform_interface/types.dart'; import 'messages.g.dart'; import 'shared_preferences_async_android.dart'; +import 'strings.dart'; /// The Android implementation of [SharedPreferencesStorePlatform]. /// @@ -17,9 +20,11 @@ class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { /// Creates a new plugin implementation instance. SharedPreferencesAndroid({ @visibleForTesting SharedPreferencesApi? api, - }) : _api = api ?? SharedPreferencesApi(); + }) : api = api ?? SharedPreferencesApi(); - final SharedPreferencesApi _api; + /// The pigeon API used to send messages to the platform. + @visibleForTesting + final SharedPreferencesApi api; /// Registers this class as the default instance of [SharedPreferencesStorePlatform]. static void registerWith() { @@ -32,22 +37,23 @@ class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { @override Future remove(String key) async { - return _api.remove(key); + return api.remove(key); } @override Future setValue(String valueType, String key, Object value) async { switch (valueType) { case 'String': - return _api.setString(key, value as String); + return api.setString(key, value as String); case 'Bool': - return _api.setBool(key, value as bool); + return api.setBool(key, value as bool); case 'Int': - return _api.setInt(key, value as int); + return api.setInt(key, value as int); case 'Double': - return _api.setDouble(key, value as double); + return api.setDouble(key, value as double); case 'StringList': - return _api.setStringList(key, value as List); + return api.setEncodedStringList( + key, '$jsonListPrefix${jsonEncode(value)}'); } // TODO(tarrinneal): change to ArgumentError across all platforms. throw PlatformException( @@ -73,7 +79,7 @@ class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { @override Future clearWithParameters(ClearParameters parameters) async { final PreferencesFilter filter = parameters.filter; - return _api.clear( + return api.clear( filter.prefix, filter.allowList?.toList(), ); @@ -99,7 +105,17 @@ class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { GetAllParameters parameters) async { final PreferencesFilter filter = parameters.filter; final Map data = - await _api.getAll(filter.prefix, filter.allowList?.toList()); + await api.getAll(filter.prefix, filter.allowList?.toList()); + data.forEach((String? key, Object? value) { + if (value.runtimeType == String && + (value! as String).startsWith(jsonListPrefix)) { + data[key!] = + (jsonDecode((value as String).substring(jsonListPrefix.length)) + as List) + .cast() + .toList(); + } + }); return data.cast(); } } 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 2b6dcd5bcaa1..e7c30c114d2f 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 @@ -2,12 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:shared_preferences_platform_interface/types.dart'; import 'messages_async.g.dart'; +import 'strings.dart'; const String _listPrefix = 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu'; @@ -35,7 +38,8 @@ base class SharedPreferencesAsyncAndroid } /// Returns a SharedPreferencesPigeonOptions for sending to platform. - SharedPreferencesPigeonOptions _convertOptionsToPigeonOptions( + @visibleForTesting + SharedPreferencesPigeonOptions convertOptionsToPigeonOptions( SharedPreferencesOptions options) { if (options is SharedPreferencesAsyncAndroidOptions) { return SharedPreferencesPigeonOptions( @@ -47,7 +51,10 @@ base class SharedPreferencesAsyncAndroid return SharedPreferencesPigeonOptions(); } - SharedPreferencesAsyncApi _getApiForBackend( + /// Provides the backend (SharedPreferences or DataStore) required based on + /// the passed in [SharedPreferencesPigeonOptions]. + @visibleForTesting + SharedPreferencesAsyncApi getApiForBackend( SharedPreferencesPigeonOptions options) { return options.useDataStore ? _dataStoreApi : _sharedPreferencesApi; } @@ -59,8 +66,8 @@ base class SharedPreferencesAsyncAndroid ) async { final PreferencesFilters filter = parameters.filter; final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return (await api.getKeys( filter.allowList?.toList(), pigeonOptions, @@ -79,8 +86,8 @@ base class SharedPreferencesAsyncAndroid 'StorageError: This string cannot be stored as it clashes with special identifier prefixes'); } final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return api.setString(key, value, pigeonOptions); } @@ -92,8 +99,8 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return api.setInt(key, value, pigeonOptions); } @@ -104,8 +111,8 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return api.setDouble(key, value, pigeonOptions); } @@ -116,8 +123,8 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return api.setBool(key, value, pigeonOptions); } @@ -128,9 +135,10 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); - return api.setStringList(key, value, pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); + final String stringValue = '$jsonListPrefix${jsonEncode(value)}'; + return api.setString(key, stringValue, pigeonOptions); } @override @@ -139,8 +147,8 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return _convertKnownExceptions( () async => api.getString(key, pigeonOptions)); } @@ -151,8 +159,8 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return _convertKnownExceptions( () async => api.getBool(key, pigeonOptions)); } @@ -163,8 +171,8 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return _convertKnownExceptions( () async => api.getDouble(key, pigeonOptions)); } @@ -175,8 +183,8 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return _convertKnownExceptions( () async => api.getInt(key, pigeonOptions)); } @@ -187,12 +195,28 @@ base class SharedPreferencesAsyncAndroid SharedPreferencesOptions options, ) async { final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); - // TODO(tarrinneal): Remove cast once https://github.com/flutter/flutter/issues/97848 - // is fixed. In practice, the values will never be null, and the native implementation assumes that. - return _convertKnownExceptions>(() async => - (await api.getStringList(key, pigeonOptions))?.cast().toList()); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); + // Request JSON encoded string list. + final String? jsonEncodedStringList = + 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) { + 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 { @@ -215,8 +239,8 @@ base class SharedPreferencesAsyncAndroid ) async { final PreferencesFilters filter = parameters.filter; final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); return api.clear( filter.allowList?.toList(), pigeonOptions, @@ -230,12 +254,19 @@ base class SharedPreferencesAsyncAndroid ) async { final PreferencesFilters filter = parameters.filter; final SharedPreferencesPigeonOptions pigeonOptions = - _convertOptionsToPigeonOptions(options); - final SharedPreferencesAsyncApi api = _getApiForBackend(pigeonOptions); + convertOptionsToPigeonOptions(options); + final SharedPreferencesAsyncApi api = getApiForBackend(pigeonOptions); final Map data = await api.getAll( filter.allowList?.toList(), pigeonOptions, ); + data.forEach((String? key, Object? value) { + if (value is String && value.startsWith(jsonListPrefix)) { + data[key!] = (jsonDecode(value.substring(jsonListPrefix.length)) + as List) + .cast(); + } + }); return data.cast(); } } diff --git a/packages/shared_preferences/shared_preferences_android/lib/src/strings.dart b/packages/shared_preferences/shared_preferences_android/lib/src/strings.dart new file mode 100644 index 000000000000..78c37292d992 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/lib/src/strings.dart @@ -0,0 +1,18 @@ +// 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. + +/// String prefix for lists that are encoded on the platform. +const String listPrefix = 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu'; + +/// String prefix for lists that are encoded with json in dart. +/// +/// The addition of the symbol `!` was chosen as it can't be created by the +/// base 64 encoding used with [listPrefix]. +const String jsonListPrefix = 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu!'; + +/// String prefix for doubles that are encoded as strings on the platform. +const String doublePrefix = 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu'; + +/// String prefix for big ints that are encoded as strings on the platform. +const String bigIntPrefix = 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy'; diff --git a/packages/shared_preferences/shared_preferences_android/pigeons/messages.dart b/packages/shared_preferences/shared_preferences_android/pigeons/messages.dart index e359c3266831..4d5858367e12 100644 --- a/packages/shared_preferences/shared_preferences_android/pigeons/messages.dart +++ b/packages/shared_preferences/shared_preferences_android/pigeons/messages.dart @@ -37,7 +37,13 @@ abstract class SharedPreferencesApi { /// Adds property to shared preferences data set of type List. @TaskQueue(type: TaskQueueType.serialBackgroundThread) - bool setStringList(String key, List value); + bool setEncodedStringList(String key, String value); + + /// Adds property to shared preferences data set of type List. + /// + /// Deprecated, this is only here for testing purposes. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + bool setDeprecatedStringList(String key, List value); /// Removes all properties from shared preferences data set with matching prefix. @TaskQueue(type: TaskQueueType.serialBackgroundThread) 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 9334123482b5..66680044e325 100644 --- a/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart +++ b/packages/shared_preferences/shared_preferences_android/pigeons/messages_async.dart @@ -56,7 +56,17 @@ abstract class SharedPreferencesAsyncApi { /// Adds property to shared preferences data set of type List. @TaskQueue(type: TaskQueueType.serialBackgroundThread) - void setStringList( + void setEncodedStringList( + String key, + String value, + SharedPreferencesPigeonOptions options, + ); + + /// Adds property to shared preferences data set of type List. + /// + /// Deprecated, this is only here for testing purposes. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + void setDeprecatedStringList( String key, List value, SharedPreferencesPigeonOptions options, @@ -92,7 +102,14 @@ abstract class SharedPreferencesAsyncApi { /// Gets individual List value stored with [key], if any. @TaskQueue(type: TaskQueueType.serialBackgroundThread) - List? getStringList( + List? getPlatformEncodedStringList( + String key, + SharedPreferencesPigeonOptions options, + ); + + /// Gets individual List value stored with [key], if any. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + String? 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 e2f143a4a87e..bda1bd0c430a 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.2 +version: 2.4.3 environment: sdk: ^3.5.0 @@ -25,7 +25,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pigeon: ^22.6.0 + pigeon: ^22.7.2 topics: - persistence diff --git a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart index c30fa08d18d2..ce42b4b18363 100644 --- a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart +++ b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences_android/shared_preferences_android.dart'; import 'package:shared_preferences_android/src/messages.g.dart'; +import 'package:shared_preferences_android/src/strings.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_platform_interface/types.dart'; @@ -38,11 +41,22 @@ void main() { 'StringList': ['foo', 'bar'], }; - final Map allTestValues = {}; + final Map allTestValuesForComparison = {}; + + allTestValuesForComparison.addAll(flutterTestValues); + allTestValuesForComparison.addAll(prefixTestValues); + allTestValuesForComparison.addAll(nonPrefixTestValues); - allTestValues.addAll(flutterTestValues); - allTestValues.addAll(prefixTestValues); - allTestValues.addAll(nonPrefixTestValues); + final Map allTestValuesForAddingDirectlyToCache = + {...allTestValuesForComparison}; + + final String encodedListStringValue = + '$jsonListPrefix${jsonEncode(['foo', 'bar'])}'; + allTestValuesForAddingDirectlyToCache['flutter.StringList'] = + encodedListStringValue; + allTestValuesForAddingDirectlyToCache['prefix.StringList'] = + encodedListStringValue; + allTestValuesForAddingDirectlyToCache['StringList'] = encodedListStringValue; setUp(() { api = _FakeSharedPreferencesApi(); @@ -68,8 +82,8 @@ void main() { }); test('clearWithPrefix', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } Map all = await plugin.getAllWithPrefix('prefix.'); @@ -82,8 +96,8 @@ void main() { }); test('clearWithParameters', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } Map all = await plugin.getAllWithParameters( @@ -108,8 +122,8 @@ void main() { }); test('clearWithParameters with allow list', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } Map all = await plugin.getAllWithParameters( @@ -146,17 +160,17 @@ void main() { }); test('getAllWithNoPrefix', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } final Map all = await plugin.getAllWithPrefix(''); expect(all.length, 15); - expect(all, allTestValues); + expect(all, allTestValuesForComparison); }); test('clearWithNoPrefix', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } Map all = await plugin.getAllWithPrefix(''); @@ -167,8 +181,8 @@ void main() { }); test('getAllWithParameters', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } final Map all = await plugin.getAllWithParameters( GetAllParameters( @@ -180,8 +194,8 @@ void main() { }); test('getAllWithParameters with allow list', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } final Map all = await plugin.getAllWithParameters( GetAllParameters( @@ -208,7 +222,8 @@ void main() { await plugin .setValue('StringList', 'flutter.StringList', ['hi']), isTrue); - expect(api.items['flutter.StringList'], ['hi']); + expect(api.items['flutter.StringList'], + '$jsonListPrefix${jsonEncode(['hi'])}'); }); test('setValue with unsupported type', () async { @@ -218,8 +233,8 @@ void main() { }); test('getAllWithNoPrefix', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } final Map all = await plugin.getAllWithParameters( GetAllParameters( @@ -227,12 +242,12 @@ void main() { ), ); expect(all.length, 15); - expect(all, allTestValues); + expect(all, allTestValuesForComparison); }); test('clearWithNoPrefix', () async { - for (final String key in allTestValues.keys) { - api.items[key] = allTestValues[key]!; + for (final String key in allTestValuesForAddingDirectlyToCache.keys) { + api.items[key] = allTestValuesForAddingDirectlyToCache[key]!; } Map all = await plugin.getAllWithParameters( @@ -259,7 +274,7 @@ class _FakeSharedPreferencesApi implements SharedPreferencesApi { final Map items = {}; @override - Future> getAll( + Future> getAll( String prefix, List? allowList, ) async { @@ -267,12 +282,23 @@ class _FakeSharedPreferencesApi implements SharedPreferencesApi { if (allowList != null) { allowSet = Set.from(allowList); } - return { + final Map filteredItems = { for (final String key in items.keys) if (key.startsWith(prefix) && (allowSet == null || allowSet.contains(key))) - key: items[key] + key: items[key]! }; + filteredItems.forEach((String? key, Object? value) { + if (value.runtimeType == String && + (value! as String).startsWith(jsonListPrefix)) { + filteredItems[key!] = + (jsonDecode((value as String).substring(jsonListPrefix.length)) + as List) + .cast() + .toList(); + } + }); + return filteredItems; } @override @@ -317,8 +343,22 @@ class _FakeSharedPreferencesApi implements SharedPreferencesApi { } @override - Future setStringList(String key, List value) async { + Future setEncodedStringList(String key, String value) async { + items[key] = value; + return true; + } + + @override + Future setDeprecatedStringList(String key, List value) async { items[key] = value; return true; } + + @override + // ignore: non_constant_identifier_names + BinaryMessenger? get pigeonVar_binaryMessenger => throw UnimplementedError(); + + @override + // ignore: non_constant_identifier_names + String get pigeonVar_messageChannelSuffix => throw UnimplementedError(); } 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 fd2907e164b1..404c0439a9dd 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 @@ -4,10 +4,13 @@ // ignore_for_file: non_constant_identifier_names +import 'dart:convert'; + import 'package:flutter/src/services/binary_messenger.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences_android/shared_preferences_android.dart'; import 'package:shared_preferences_android/src/messages_async.g.dart'; +import 'package:shared_preferences_android/src/strings.dart'; import 'package:shared_preferences_platform_interface/types.dart'; void main() { @@ -83,7 +86,10 @@ void main() { getPreferences(useDataStore); await preferences.setStringList(listKey, testList, emptyOptions); - expect(await preferences.getStringList(listKey, emptyOptions), testList); + final List? response = + await preferences.getStringList(listKey, emptyOptions); + + expect(response, testList); }); test('getPreferences with $backend', () async { @@ -292,7 +298,13 @@ class _FakeSharedPreferencesApi implements SharedPreferencesAsyncApi { } @override - Future?> getStringList( + Future getStringList( + String key, SharedPreferencesPigeonOptions options) async { + return items[key] as String?; + } + + @override + Future?> getPlatformEncodedStringList( String key, SharedPreferencesPigeonOptions options) async { return items[key] as List?; } @@ -326,7 +338,14 @@ class _FakeSharedPreferencesApi implements SharedPreferencesAsyncApi { } @override - Future setStringList(String key, List value, + Future setEncodedStringList( + String key, String value, SharedPreferencesPigeonOptions options) async { + items[key] = '$jsonListPrefix${jsonEncode(value)}'; + return true; + } + + @override + Future setDeprecatedStringList(String key, List value, SharedPreferencesPigeonOptions options) async { items[key] = value; return true;