diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index 044aedfec29a..9ba3e781ba45 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.1 + +* Updates minimum Flutter version to 3.0. +* Converts implementation to Pigeon. + ## 2.1.0 * Adds `getAllWithPrefix` and `clearWithPrefix` methods. 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 new file mode 100644 index 000000000000..20efb4404d40 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/Messages.java @@ -0,0 +1,317 @@ +// 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 (v9.2.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.sharedpreferences; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) +public class Messages { + + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface SharedPreferencesApi { + /** Removes property from shared preferences data set. */ + @NonNull + Boolean remove(@NonNull String key); + /** Adds property to shared preferences data set of type bool. */ + @NonNull + Boolean setBool(@NonNull String key, @NonNull Boolean value); + /** Adds property to shared preferences data set of type String. */ + @NonNull + Boolean setString(@NonNull String key, @NonNull String value); + /** Adds property to shared preferences data set of type int. */ + @NonNull + Boolean setInt(@NonNull String key, @NonNull Long value); + /** Adds property to shared preferences data set of type double. */ + @NonNull + 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); + /** Removes all properties from shared preferences data set with matching prefix. */ + @NonNull + Boolean clearWithPrefix(@NonNull String prefix); + /** Gets all properties from shared preferences data set with matching prefix. */ + @NonNull + Map getAllWithPrefix(@NonNull String prefix); + + /** The codec used by SharedPreferencesApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `SharedPreferencesApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable SharedPreferencesApi api) { + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SharedPreferencesApi.remove", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + 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; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SharedPreferencesApi.setBool", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String keyArg = (String) args.get(0); + Boolean valueArg = (Boolean) args.get(1); + try { + Boolean output = api.setBool(keyArg, valueArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SharedPreferencesApi.setString", + 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.setString(keyArg, valueArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SharedPreferencesApi.setInt", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String keyArg = (String) args.get(0); + Number valueArg = (Number) args.get(1); + try { + Boolean output = + api.setInt(keyArg, (valueArg == null) ? null : valueArg.longValue()); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SharedPreferencesApi.setDouble", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String keyArg = (String) args.get(0); + Double valueArg = (Double) args.get(1); + try { + Boolean output = api.setDouble(keyArg, valueArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SharedPreferencesApi.setStringList", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + 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); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SharedPreferencesApi.clearWithPrefix", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String prefixArg = (String) args.get(0); + try { + Boolean output = api.clearWithPrefix(prefixArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SharedPreferencesApi.getAllWithPrefix", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String prefixArg = (String) args.get(0); + try { + Map output = api.getAllWithPrefix(prefixArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } +} diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java deleted file mode 100644 index 7bdc8e7239c5..000000000000 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ /dev/null @@ -1,226 +0,0 @@ -// 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. - -package io.flutter.plugins.sharedpreferences; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Handler; -import android.os.Looper; -import android.util.Base64; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Implementation of the {@link MethodChannel.MethodCallHandler} for the plugin. It is also - * responsible of managing the {@link android.content.SharedPreferences}. - */ -@SuppressWarnings("unchecked") -class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - - private static final String SHARED_PREFERENCES_NAME = "FlutterSharedPreferences"; - - // Fun fact: The following is a base64 encoding of the string "This is the prefix for a list." - private static final String LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"; - private static final String BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy"; - private static final String DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu"; - - private final android.content.SharedPreferences preferences; - - private final ExecutorService executor; - private final Handler handler; - - /** - * Constructs a {@link MethodCallHandlerImpl} instance. Creates a {@link - * android.content.SharedPreferences} based on the {@code context}. - */ - MethodCallHandlerImpl(Context context) { - preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); - executor = - new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue()); - handler = new Handler(Looper.getMainLooper()); - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - String key = call.argument("key"); - try { - switch (call.method) { - case "setBool": - commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument("value")), result); - break; - case "setDouble": - double doubleValue = ((Number) call.argument("value")).doubleValue(); - String doubleValueStr = Double.toString(doubleValue); - commitAsync(preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr), result); - break; - case "setInt": - Number number = call.argument("value"); - if (number instanceof BigInteger) { - BigInteger integerValue = (BigInteger) number; - commitAsync( - preferences - .edit() - .putString( - key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX)), - result); - } else { - commitAsync(preferences.edit().putLong(key, number.longValue()), result); - } - break; - case "setString": - String value = (String) call.argument("value"); - if (value.startsWith(LIST_IDENTIFIER) - || value.startsWith(BIG_INTEGER_PREFIX) - || value.startsWith(DOUBLE_PREFIX)) { - result.error( - "StorageError", - "This string cannot be stored as it clashes with special identifier prefixes.", - null); - return; - } - commitAsync(preferences.edit().putString(key, value), result); - break; - case "setStringList": - List list = call.argument("value"); - commitAsync( - preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result); - break; - case "commit": - // We've been committing the whole time. - result.success(true); - break; - case "getAllWithPrefix": - String prefix = call.argument("prefix"); - result.success(getAllPrefs(prefix)); - return; - case "remove": - commitAsync(preferences.edit().remove(key), result); - break; - case "clearWithPrefix": - String newPrefix = call.argument("prefix"); - Set keys = getAllPrefs(newPrefix).keySet(); - SharedPreferences.Editor clearEditor = preferences.edit(); - for (String keyToDelete : keys) { - clearEditor.remove(keyToDelete); - } - commitAsync(clearEditor, result); - break; - default: - result.notImplemented(); - break; - } - } catch (IOException e) { - result.error("IOException encountered", call.method, e); - } - } - - public void teardown() { - handler.removeCallbacksAndMessages(null); - executor.shutdown(); - } - - private void commitAsync( - final SharedPreferences.Editor editor, final MethodChannel.Result result) { - executor.execute( - new Runnable() { - @Override - public void run() { - final boolean response = editor.commit(); - handler.post( - new Runnable() { - @Override - public void run() { - result.success(response); - } - }); - } - }); - } - - private List decodeList(String encodedList) throws IOException { - ObjectInputStream stream = null; - try { - stream = new ObjectInputStream(new ByteArrayInputStream(Base64.decode(encodedList, 0))); - return (List) stream.readObject(); - } catch (ClassNotFoundException e) { - throw new IOException(e); - } finally { - if (stream != null) { - stream.close(); - } - } - } - - private String encodeList(List list) throws IOException { - ObjectOutputStream stream = null; - try { - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - stream = new ObjectOutputStream(byteStream); - stream.writeObject(list); - stream.flush(); - return Base64.encodeToString(byteStream.toByteArray(), 0); - } finally { - if (stream != null) { - stream.close(); - } - } - } - - // Gets all shared preferences, filtered to only those set with the given prefix. - private Map getAllPrefs(String prefix) throws IOException { - Map allPrefs = preferences.getAll(); - Map filteredPrefs = new HashMap<>(); - for (String key : allPrefs.keySet()) { - if (key.startsWith(prefix)) { - Object value = allPrefs.get(key); - if (value instanceof String) { - String stringValue = (String) value; - if (stringValue.startsWith(LIST_IDENTIFIER)) { - value = decodeList(stringValue.substring(LIST_IDENTIFIER.length())); - } else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) { - String encoded = stringValue.substring(BIG_INTEGER_PREFIX.length()); - value = new BigInteger(encoded, Character.MAX_RADIX); - } else if (stringValue.startsWith(DOUBLE_PREFIX)) { - String doubleStr = stringValue.substring(DOUBLE_PREFIX.length()); - value = Double.valueOf(doubleStr); - } - } else if (value instanceof Set) { - // This only happens for previous usage of setStringSet. The app expects a list. - List listValue = new ArrayList<>((Set) value); - // Let's migrate the value too while we are at it. - boolean success = - preferences - .edit() - .remove(key) - .putString(key, LIST_IDENTIFIER + encodeList(listValue)) - .commit(); - if (!success) { - // If we are unable to migrate the existing preferences, it means we potentially lost them. - // In this case, an error from getAllPrefs() is appropriate since it will alert the app during plugin initialization. - throw new IOException("Could not migrate set to list"); - } - value = listValue; - } - filteredPrefs.put(key, value); - } - } - return filteredPrefs; - } -} diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.java new file mode 100644 index 000000000000..04d1bcff344f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.java @@ -0,0 +1,21 @@ +// 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. + +package io.flutter.plugins.sharedpreferences; + +import androidx.annotation.NonNull; +import java.util.List; + +/** + * An interface used to provide conversion logic between List and String for + * SharedPreferencesPlugin. + */ +public interface SharedPreferencesListEncoder { + /** Converts list to String for storing in shared preferences. */ + @NonNull + String encode(@NonNull List list); + /** Converts stored String representing List to List. */ + @NonNull + List decode(@NonNull String listString); +} diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java index 9545fe95c54b..6f59b720ff3f 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java @@ -5,42 +5,204 @@ package io.flutter.plugins.sharedpreferences; import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.sharedpreferences.Messages.SharedPreferencesApi; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; /** SharedPreferencesPlugin */ -public class SharedPreferencesPlugin implements FlutterPlugin { - private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences_android"; - private MethodChannel channel; - private MethodCallHandlerImpl handler; +public class SharedPreferencesPlugin implements FlutterPlugin, SharedPreferencesApi { + private static final String TAG = "SharedPreferencesPlugin"; + private static final String SHARED_PREFERENCES_NAME = "FlutterSharedPreferences"; + private static final String LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"; + private static final String BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy"; + private static final String DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu"; + + private SharedPreferences preferences; + private SharedPreferencesListEncoder listEncoder; + + public SharedPreferencesPlugin() { + this(new ListEncoder()); + } + + @VisibleForTesting + SharedPreferencesPlugin(@NonNull SharedPreferencesListEncoder listEncoder) { + this.listEncoder = listEncoder; + } @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + public static void registerWith( + @NonNull io.flutter.plugin.common.PluginRegistry.Registrar registrar) { final SharedPreferencesPlugin plugin = new SharedPreferencesPlugin(); - plugin.setupChannel(registrar.messenger(), registrar.context()); + plugin.setUp(registrar.messenger(), registrar.context()); + } + + private void setUp(@NonNull BinaryMessenger messenger, @NonNull Context context) { + preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + try { + SharedPreferencesApi.setup(messenger, this); + } catch (Exception ex) { + Log.e(TAG, "Received exception while setting up SharedPreferencesPlugin", ex); + } } @Override - public void onAttachedToEngine(FlutterPlugin.FlutterPluginBinding binding) { - setupChannel(binding.getBinaryMessenger(), binding.getApplicationContext()); + public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { + setUp(binding.getBinaryMessenger(), binding.getApplicationContext()); } @Override - public void onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding binding) { - teardownChannel(); + public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { + SharedPreferencesApi.setup(binding.getBinaryMessenger(), null); } - private void setupChannel(BinaryMessenger messenger, Context context) { - channel = new MethodChannel(messenger, CHANNEL_NAME); - handler = new MethodCallHandlerImpl(context); - channel.setMethodCallHandler(handler); + @Override + public @NonNull Boolean setBool(@NonNull String key, @NonNull Boolean value) { + return preferences.edit().putBoolean(key, value).commit(); } - private void teardownChannel() { - handler.teardown(); - handler = null; - channel.setMethodCallHandler(null); - channel = null; + @Override + public @NonNull Boolean setString(@NonNull String key, @NonNull String value) { + // TODO (tarrinneal): Move this string prefix checking logic to dart code and make it an Argument Error. + if (value.startsWith(LIST_IDENTIFIER) + || value.startsWith(BIG_INTEGER_PREFIX) + || value.startsWith(DOUBLE_PREFIX)) { + throw new RuntimeException( + "StorageError: This string cannot be stored as it clashes with special identifier prefixes"); + } + return preferences.edit().putString(key, value).commit(); + } + + @Override + public @NonNull Boolean setInt(@NonNull String key, @NonNull Long value) { + return preferences.edit().putLong(key, value).commit(); + } + + @Override + public @NonNull Boolean setDouble(@NonNull String key, @NonNull Double value) { + String doubleValueStr = Double.toString(value); + return preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit(); + } + + @Override + public @NonNull Boolean remove(@NonNull String key) { + return preferences.edit().remove(key).commit(); + } + + @Override + public @NonNull Boolean setStringList(@NonNull String key, @NonNull List value) + throws RuntimeException { + return preferences.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)).commit(); + } + + @Override + public @NonNull Map getAllWithPrefix(@NonNull String prefix) + throws RuntimeException { + return getAllPrefs(prefix); + } + + @Override + public @NonNull Boolean clearWithPrefix(@NonNull String prefix) throws RuntimeException { + SharedPreferences.Editor clearEditor = preferences.edit(); + Map allPrefs = preferences.getAll(); + ArrayList filteredPrefs = new ArrayList<>(); + for (String key : allPrefs.keySet()) { + if (key.startsWith(prefix)) { + filteredPrefs.add(key); + } + } + for (String key : filteredPrefs) { + clearEditor.remove(key); + } + return clearEditor.commit(); + } + + // Gets all shared preferences, filtered to only those set with the given prefix. + @SuppressWarnings("unchecked") + private @NonNull Map getAllPrefs(@NonNull String prefix) throws RuntimeException { + Map allPrefs = preferences.getAll(); + Map filteredPrefs = new HashMap<>(); + for (String key : allPrefs.keySet()) { + if (key.startsWith(prefix)) { + filteredPrefs.put(key, transformPref(key, allPrefs.get(key))); + } + } + + return filteredPrefs; + } + + 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())); + } else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) { + // TODO (tarrinneal): Remove all BigInt code. + // https://github.com/flutter/flutter/issues/124420 + String encoded = stringValue.substring(BIG_INTEGER_PREFIX.length()); + return new BigInteger(encoded, Character.MAX_RADIX); + } else if (stringValue.startsWith(DOUBLE_PREFIX)) { + String doubleStr = stringValue.substring(DOUBLE_PREFIX.length()); + return Double.valueOf(doubleStr); + } + } else if (value instanceof Set) { + // TODO (tarrinneal): Remove Set code. + // https://github.com/flutter/flutter/issues/124420 + + // This only happens for previous usage of setStringSet. The app expects a list. + @SuppressWarnings("unchecked") + List listValue = new ArrayList<>((Set) value); + // Let's migrate the value too while we are at it. + preferences + .edit() + .remove(key) + .putString(key, LIST_IDENTIFIER + listEncoder.encode(listValue)) + .apply(); + + return listValue; + } + return value; + } + + static class ListEncoder implements SharedPreferencesListEncoder { + @Override + public @NonNull String encode(@NonNull List list) throws RuntimeException { + try { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + ObjectOutputStream stream = new ObjectOutputStream(byteStream); + stream.writeObject(list); + stream.flush(); + return Base64.encodeToString(byteStream.toByteArray(), 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public @NonNull List decode(@NonNull String listString) throws RuntimeException { + try { + ObjectInputStream stream = + new ObjectInputStream(new ByteArrayInputStream(Base64.decode(listString, 0))); + return (List) stream.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } } } diff --git a/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java b/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java index 13d0ff8b40c1..e793ea4a7153 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java +++ b/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java @@ -4,12 +4,325 @@ package io.flutter.plugins.sharedpreferences; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; public class SharedPreferencesTest { - // This is only a placeholder test and doesn't actually initialize the plugin. + + SharedPreferencesPlugin plugin; + + @Mock BinaryMessenger mockMessenger; + @Mock FlutterPlugin.FlutterPluginBinding flutterPluginBinding; + + @Before + public void before() { + Context context = Mockito.mock(Context.class); + SharedPreferences sharedPrefs = new FakeSharedPreferences(); + + flutterPluginBinding = Mockito.mock(FlutterPlugin.FlutterPluginBinding.class); + + Mockito.when(flutterPluginBinding.getBinaryMessenger()).thenReturn(mockMessenger); + Mockito.when(flutterPluginBinding.getApplicationContext()).thenReturn(context); + Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs); + + plugin = new SharedPreferencesPlugin(new ListEncoder()); + plugin.onAttachedToEngine(flutterPluginBinding); + } + + private static final Map data = new HashMap<>(); + + static { + data.put("Language", "Java"); + data.put("Counter", 0L); + data.put("Pie", 3.14); + data.put("Names", Arrays.asList("Flutter", "Dart")); + 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.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.NewToFlutter", false); + } + + @Test + public void getAllWithPrefix() { + assertEquals(plugin.getAllWithPrefix("").size(), 0); + + addData(); + + Map flutterData = plugin.getAllWithPrefix("flutter."); + + assertEquals(flutterData.size(), 5); + 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.NewToFlutter"), false); + + Map allData = plugin.getAllWithPrefix(""); + + assertEquals(allData, data); + } + @Test - public void initPluginDoesNotThrow() { - final SharedPreferencesPlugin plugin = new SharedPreferencesPlugin(); + public void setString() { + final String key = "language"; + final String value = "Java"; + plugin.setString(key, value); + Map flutterData = plugin.getAllWithPrefix(""); + assertEquals(flutterData.get(key), value); + } + + @Test + public void setInt() { + final String key = "Counter"; + final Long value = 0L; + plugin.setInt(key, value); + Map flutterData = plugin.getAllWithPrefix(""); + assertEquals(flutterData.get(key), value); + } + + @Test + public void setDouble() { + final String key = "Pie"; + final double value = 3.14; + plugin.setDouble(key, value); + Map flutterData = plugin.getAllWithPrefix(""); + assertEquals(flutterData.get(key), value); + } + + @Test + public void setStringList() { + final String key = "Names"; + final List value = Arrays.asList("Flutter", "Dart"); + plugin.setStringList(key, value); + Map flutterData = plugin.getAllWithPrefix(""); + assertEquals(flutterData.get(key), value); + } + + @Test + public void setBool() { + final String key = "NewToFlutter"; + final boolean value = false; + plugin.setBool(key, value); + Map flutterData = plugin.getAllWithPrefix(""); + assertEquals(flutterData.get(key), value); + } + + @Test + public void clearWithPrefix() { + addData(); + + assertEquals(plugin.getAllWithPrefix("").size(), 15); + + plugin.clearWithPrefix("flutter."); + + assertEquals(plugin.getAllWithPrefix("").size(), 10); + } + + @Test + public void clearAll() { + addData(); + + assertEquals(plugin.getAllWithPrefix("").size(), 15); + + plugin.clearWithPrefix(""); + + assertEquals(plugin.getAllWithPrefix("").size(), 0); + } + + @Test + public void testRemove() { + final String key = "NewToFlutter"; + final boolean value = true; + plugin.setBool(key, value); + assert (plugin.getAllWithPrefix("").containsKey(key)); + plugin.remove(key); + assertFalse(plugin.getAllWithPrefix("").containsKey(key)); + } + + private void addData() { + plugin.setString("Language", "Java"); + plugin.setInt("Counter", 0L); + plugin.setDouble("Pie", 3.14); + plugin.setStringList("Names", Arrays.asList("Flutter", "Dart")); + 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.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.setBool("prefix.NewToFlutter", false); + } + + /** A dummy implementation for tests for use with FakeSharedPreferences */ + public static class FakeSharedPreferencesEditor implements SharedPreferences.Editor { + private final Map sharedPrefData; + + FakeSharedPreferencesEditor(@NonNull Map data) { + sharedPrefData = data; + } + + @Override + public @NonNull SharedPreferences.Editor putString(@NonNull String key, @NonNull String value) { + sharedPrefData.put(key, value); + return this; + } + + @Override + public @NonNull SharedPreferences.Editor putStringSet( + @NonNull String key, @NonNull Set values) { + sharedPrefData.put(key, values); + return this; + } + + @Override + public @NonNull SharedPreferences.Editor putBoolean( + @NonNull String key, @NonNull boolean value) { + sharedPrefData.put(key, value); + return this; + } + + @Override + public @NonNull SharedPreferences.Editor putInt(@NonNull String key, @NonNull int value) { + sharedPrefData.put(key, value); + return this; + } + + @Override + public @NonNull SharedPreferences.Editor putLong(@NonNull String key, @NonNull long value) { + sharedPrefData.put(key, value); + return this; + } + + @Override + public @NonNull SharedPreferences.Editor putFloat(@NonNull String key, @NonNull float value) { + sharedPrefData.put(key, value); + return this; + } + + @Override + public @NonNull SharedPreferences.Editor remove(@NonNull String key) { + sharedPrefData.remove(key); + return this; + } + + @Override + public @NonNull boolean commit() { + return true; + } + + @Override + public void apply() { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public @NonNull SharedPreferences.Editor clear() { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + } + + /** A dummy implementation of SharedPreferences for tests that store values in memory. */ + private static class FakeSharedPreferences implements SharedPreferences { + + Map sharedPrefData = new HashMap<>(); + + @Override + public @NonNull Map getAll() { + return sharedPrefData; + } + + @Override + public @NonNull SharedPreferences.Editor edit() { + return new FakeSharedPreferencesEditor(sharedPrefData); + } + + // All methods below are not implemented. + @Override + public @NonNull boolean contains(@NonNull String key) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public @NonNull boolean getBoolean(@NonNull String key, @NonNull boolean defValue) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public @NonNull float getFloat(@NonNull String key, @NonNull float defValue) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public @NonNull int getInt(@NonNull String key, @NonNull int defValue) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public @NonNull long getLong(@NonNull String key, @NonNull long defValue) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public @NonNull String getString(@NonNull String key, @NonNull String defValue) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public @NonNull Set getStringSet(@NonNull String key, @NonNull Set defValues) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public void registerOnSharedPreferenceChangeListener( + @NonNull SharedPreferences.OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + @NonNull SharedPreferences.OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("This method is not implemented for testing"); + } + } + + /** A dummy implementation of SharedPreferencesListEncoder for tests that store List. */ + static class ListEncoder implements SharedPreferencesListEncoder { + @Override + public @NonNull String encode(@NonNull List list) { + return String.join(";-;", list); + } + + @Override + public @NonNull List decode(@NonNull String listString) { + return Arrays.asList(listString.split(";-;")); + } } } diff --git a/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart b/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart index b77db6c14cc0..9c26a03f53cc 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart @@ -2,18 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; -const MethodChannel _kChannel = - MethodChannel('plugins.flutter.io/shared_preferences_android'); +import 'src/messages.g.dart'; /// The Android implementation of [SharedPreferencesStorePlatform]. /// /// This class implements the `package:shared_preferences` functionality for Android. class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { + /// Creates a new plugin implementation instance. + SharedPreferencesAndroid({ + @visibleForTesting SharedPreferencesApi? api, + }) : _api = api ?? SharedPreferencesApi(); + + final SharedPreferencesApi _api; + /// Registers this class as the default instance of [SharedPreferencesStorePlatform]. static void registerWith() { SharedPreferencesStorePlatform.instance = SharedPreferencesAndroid(); @@ -23,44 +28,47 @@ class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { @override Future remove(String key) async { - return (await _kChannel.invokeMethod( - 'remove', - {'key': key}, - ))!; + return _api.remove(key); } @override Future setValue(String valueType, String key, Object value) async { - return (await _kChannel.invokeMethod( - 'set$valueType', - {'key': key, 'value': value}, - ))!; + switch (valueType) { + case 'String': + return _api.setString(key, value as String); + case 'Bool': + return _api.setBool(key, value as bool); + case 'Int': + return _api.setInt(key, value as int); + case 'Double': + return _api.setDouble(key, value as double); + case 'StringList': + return _api.setStringList(key, value as List); + } + // TODO(tarrinneal): change to ArgumentError across all platforms. + throw PlatformException( + code: 'InvalidOperation', + message: '"$valueType" is not a supported type.'); } @override - Future clear() async { + Future clear() { return clearWithPrefix(_defaultPrefix); } @override Future clearWithPrefix(String prefix) async { - return (await _kChannel.invokeMethod( - 'clearWithPrefix', - {'prefix': prefix}, - ))!; + return _api.clearWithPrefix(prefix); } @override - Future> getAll() async { + Future> getAll() { return getAllWithPrefix(_defaultPrefix); } @override Future> getAllWithPrefix(String prefix) async { - return (await _kChannel.invokeMapMethod( - 'getAllWithPrefix', - {'prefix': prefix}, - )) ?? - {}; + final Map data = await _api.getAllWithPrefix(prefix); + return data.cast(); } } 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 new file mode 100644 index 000000000000..4bb59d60621f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/lib/src/messages.g.dart @@ -0,0 +1,247 @@ +// 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 (v9.2.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +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}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Removes property from shared preferences data set. + Future remove(String arg_key) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SharedPreferencesApi.remove', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_key]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Adds property to shared preferences data set of type bool. + Future setBool(String arg_key, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SharedPreferencesApi.setBool', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_key, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Adds property to shared preferences data set of type String. + Future setString(String arg_key, String arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SharedPreferencesApi.setString', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_key, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Adds property to shared preferences data set of type int. + Future setInt(String arg_key, int arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SharedPreferencesApi.setInt', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_key, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Adds property to shared preferences data set of type double. + Future setDouble(String arg_key, double arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SharedPreferencesApi.setDouble', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_key, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Adds property to shared preferences data set of type List. + Future setStringList(String arg_key, List arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SharedPreferencesApi.setStringList', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_key, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Removes all properties from shared preferences data set with matching prefix. + Future clearWithPrefix(String arg_prefix) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SharedPreferencesApi.clearWithPrefix', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_prefix]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Gets all properties from shared preferences data set with matching prefix. + Future> getAllWithPrefix(String arg_prefix) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SharedPreferencesApi.getAllWithPrefix', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_prefix]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as Map?)!.cast(); + } + } +} diff --git a/packages/shared_preferences/shared_preferences_android/pigeons/copyright.txt b/packages/shared_preferences/shared_preferences_android/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/shared_preferences/shared_preferences_android/pigeons/messages.dart b/packages/shared_preferences/shared_preferences_android/pigeons/messages.dart new file mode 100644 index 000000000000..6bff14700a59 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/pigeons/messages.dart @@ -0,0 +1,49 @@ +// 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. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + input: 'pigeons/messages.dart', + javaOut: + 'android/src/main/java/io/flutter/plugins/sharedpreferences/Messages.java', + javaOptions: JavaOptions( + className: 'Messages', package: 'io.flutter.plugins.sharedpreferences'), + dartOut: 'lib/src/messages.g.dart', + copyrightHeader: 'pigeons/copyright.txt', +)) +@HostApi(dartHostTestHandler: 'TestSharedPreferencesApi') +abstract class SharedPreferencesApi { + /// Removes property from shared preferences data set. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + bool remove(String key); + + /// Adds property to shared preferences data set of type bool. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + bool setBool(String key, bool value); + + /// Adds property to shared preferences data set of type String. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + bool setString(String key, String value); + + /// Adds property to shared preferences data set of type int. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + bool setInt(String key, int value); + + /// Adds property to shared preferences data set of type double. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + bool setDouble(String key, double value); + + /// Adds property to shared preferences data set of type List. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + bool setStringList(String key, List value); + + /// Removes all properties from shared preferences data set with matching prefix. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + bool clearWithPrefix(String prefix); + + /// Gets all properties from shared preferences data set with matching prefix. + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + Map getAllWithPrefix(String prefix); +} diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index 02dd43abf95e..570f9931257a 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.1.0 +version: 2.1.1 environment: sdk: ">=2.17.0 <4.0.0" @@ -25,3 +25,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^9.2.3 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 9c4ce2111d79..2c5030462727 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 @@ -5,195 +5,197 @@ 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_platform_interface/method_channel_shared_preferences.dart'; +import 'package:shared_preferences_android/src/messages.g.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + late _FakeSharedPreferencesApi api; + late SharedPreferencesAndroid plugin; + + const Map flutterTestValues = { + 'flutter.String': 'hello world', + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], + }; + + const Map prefixTestValues = { + 'prefix.String': 'hello world', + 'prefix.Bool': true, + 'prefix.Int': 42, + 'prefix.Double': 3.14159, + 'prefix.StringList': ['foo', 'bar'], + }; + + const Map nonPrefixTestValues = { + 'String': 'hello world', + 'Bool': true, + 'Int': 42, + 'Double': 3.14159, + 'StringList': ['foo', 'bar'], + }; + + final Map allTestValues = {}; + + allTestValues.addAll(flutterTestValues); + allTestValues.addAll(prefixTestValues); + allTestValues.addAll(nonPrefixTestValues); + + setUp(() { + api = _FakeSharedPreferencesApi(); + plugin = SharedPreferencesAndroid(api: api); + }); - group(MethodChannelSharedPreferencesStore, () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/shared_preferences_android', - ); - - const Map flutterTestValues = { - 'flutter.String': 'hello world', - 'flutter.Bool': true, - 'flutter.Int': 42, - 'flutter.Double': 3.14159, - 'flutter.StringList': ['foo', 'bar'], - }; - - const Map prefixTestValues = { - 'prefix.String': 'hello world', - 'prefix.Bool': true, - 'prefix.Int': 42, - 'prefix.Double': 3.14159, - 'prefix.StringList': ['foo', 'bar'], - }; - - const Map nonPrefixTestValues = { - 'String': 'hello world', - 'Bool': true, - 'Int': 42, - 'Double': 3.14159, - 'StringList': ['foo', 'bar'], - }; + test('registerWith', () { + SharedPreferencesAndroid.registerWith(); + expect(SharedPreferencesStorePlatform.instance, + isA()); + }); - final Map allTestValues = {}; + test('remove', () async { + api.items['flutter.hi'] = 'world'; + expect(await plugin.remove('flutter.hi'), isTrue); + expect(api.items.containsKey('flutter.hi'), isFalse); + }); - allTestValues.addAll(flutterTestValues); - allTestValues.addAll(prefixTestValues); - allTestValues.addAll(nonPrefixTestValues); + test('clear', () async { + api.items['flutter.hi'] = 'world'; + expect(await plugin.clear(), isTrue); + expect(api.items.containsKey('flutter.hi'), isFalse); + }); - late InMemorySharedPreferencesStore testData; + test('clearWithPrefix', () async { + for (final String key in allTestValues.keys) { + api.items[key] = allTestValues[key]!; + } + + Map all = await plugin.getAllWithPrefix('prefix.'); + expect(all.length, 5); + await plugin.clearWithPrefix('prefix.'); + all = await plugin.getAll(); + expect(all.length, 5); + all = await plugin.getAllWithPrefix('prefix.'); + expect(all.length, 0); + }); - final List log = []; - late SharedPreferencesStorePlatform store; + test('getAll', () async { + for (final String key in flutterTestValues.keys) { + api.items[key] = flutterTestValues[key]!; + } + final Map all = await plugin.getAll(); + expect(all.length, 5); + expect(all, flutterTestValues); + }); - setUp(() async { - testData = InMemorySharedPreferencesStore.empty(); + test('getAllWithPrefix', () async { + for (final String key in allTestValues.keys) { + api.items[key] = allTestValues[key]!; + } + final Map all = await plugin.getAllWithPrefix('prefix.'); + expect(all.length, 5); + expect(all, prefixTestValues); + }); - Map getArgumentDictionary(MethodCall call) { - return (call.arguments as Map) - .cast(); - } + test('setValue', () async { + expect(await plugin.setValue('Bool', 'flutter.Bool', true), isTrue); + expect(api.items['flutter.Bool'], true); + expect(await plugin.setValue('Double', 'flutter.Double', 1.5), isTrue); + expect(api.items['flutter.Double'], 1.5); + expect(await plugin.setValue('Int', 'flutter.Int', 12), isTrue); + expect(api.items['flutter.Int'], 12); + expect(await plugin.setValue('String', 'flutter.String', 'hi'), isTrue); + expect(api.items['flutter.String'], 'hi'); + expect( + await plugin + .setValue('StringList', 'flutter.StringList', ['hi']), + isTrue); + expect(api.items['flutter.StringList'], ['hi']); + }); - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - log.add(methodCall); - if (methodCall.method == 'getAll') { - return testData.getAll(); - } - if (methodCall.method == 'getAllWithPrefix') { - final Map arguments = - getArgumentDictionary(methodCall); - final String prefix = arguments['prefix']! as String; - return testData.getAllWithPrefix(prefix); - } - if (methodCall.method == 'remove') { - final Map arguments = - getArgumentDictionary(methodCall); - final String key = arguments['key']! as String; - return testData.remove(key); - } - if (methodCall.method == 'clear') { - return testData.clear(); - } - if (methodCall.method == 'clearWithPrefix') { - final Map arguments = - getArgumentDictionary(methodCall); - final String prefix = arguments['prefix']! as String; - return testData.clearWithPrefix(prefix); - } - final RegExp setterRegExp = RegExp(r'set(.*)'); - final Match? match = setterRegExp.matchAsPrefix(methodCall.method); - if (match?.groupCount == 1) { - final String valueType = match!.group(1)!; - final Map arguments = - getArgumentDictionary(methodCall); - final String key = arguments['key']! as String; - final Object value = arguments['value']!; - return testData.setValue(valueType, key, value); - } - fail('Unexpected method call: ${methodCall.method}'); - }); - log.clear(); - }); + test('setValue with unsupported type', () { + expect(() async { + await plugin.setValue('Map', 'flutter.key', {}); + }, throwsA(isA())); + }); - test('registered instance', () { - SharedPreferencesAndroid.registerWith(); - expect(SharedPreferencesStorePlatform.instance, - isA()); - }); + test('getAllWithNoPrefix', () async { + for (final String key in allTestValues.keys) { + api.items[key] = allTestValues[key]!; + } + final Map all = await plugin.getAllWithPrefix(''); + expect(all.length, 15); + expect(all, allTestValues); + }); - test('getAll', () async { - store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(allTestValues); - expect(await store.getAll(), flutterTestValues); - expect(log.single.method, 'getAllWithPrefix'); - }); + test('clearWithNoPrefix', () async { + for (final String key in allTestValues.keys) { + api.items[key] = allTestValues[key]!; + } - test('getAllWithPrefix', () async { - store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(allTestValues); - expect(await store.getAllWithPrefix('prefix.'), prefixTestValues); - expect(log.single.method, 'getAllWithPrefix'); - }); + Map all = await plugin.getAllWithPrefix(''); + expect(all.length, 15); + await plugin.clearWithPrefix(''); + all = await plugin.getAllWithPrefix(''); + expect(all.length, 0); + }); +} - test('remove', () async { - store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(allTestValues); - expect(await store.remove('flutter.String'), true); - expect(await store.remove('flutter.Bool'), true); - expect(await store.remove('flutter.Int'), true); - expect(await store.remove('flutter.Double'), true); - expect(await testData.getAll(), { - 'flutter.StringList': ['foo', 'bar'], - }); - - expect(log, hasLength(4)); - for (final MethodCall call in log) { - expect(call.method, 'remove'); - } - }); +class _FakeSharedPreferencesApi implements SharedPreferencesApi { + final Map items = {}; - test('setValue', () async { - store = SharedPreferencesAndroid(); - expect(await testData.getAll(), isEmpty); - for (final String key in allTestValues.keys) { - final Object value = allTestValues[key]!; - expect(await store.setValue(key.split('.').last, key, value), true); + @override + Future> getAllWithPrefix(String prefix) async { + return { + for (final String key in items.keys) + if (key.startsWith(prefix)) key: items[key] + }; + } + + @override + Future remove(String key) async { + items.remove(key); + return true; + } + + @override + Future setBool(String key, bool value) async { + items[key] = value; + return true; + } + + @override + Future setDouble(String key, double value) async { + items[key] = value; + return true; + } + + @override + Future clearWithPrefix(String prefix) async { + items.keys.toList().forEach((String key) { + if (key.startsWith(prefix)) { + items.remove(key); } - expect(await testData.getAll(), flutterTestValues); - - expect(log, hasLength(15)); - expect(log[0].method, 'setString'); - expect(log[1].method, 'setBool'); - expect(log[2].method, 'setInt'); - expect(log[3].method, 'setDouble'); - expect(log[4].method, 'setStringList'); - }); - - test('clear', () async { - store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(allTestValues); - expect(await testData.getAll(), isNotEmpty); - expect(await store.clear(), true); - expect(await testData.getAll(), isEmpty); - expect(log.single.method, 'clearWithPrefix'); }); - - test('clearWithPrefix', () async { - store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(allTestValues); - - expect(await testData.getAllWithPrefix('prefix.'), isNotEmpty); - expect(await store.clearWithPrefix('prefix.'), true); - expect(await testData.getAllWithPrefix('prefix.'), isEmpty); - }); - - test('getAllWithNoPrefix', () async { - store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(allTestValues); - - expect(await testData.getAllWithPrefix(''), hasLength(15)); - }); - - test('clearWithNoPrefix', () async { - store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(allTestValues); - - expect(await testData.getAllWithPrefix(''), isNotEmpty); - expect(await store.clearWithPrefix(''), true); - expect(await testData.getAllWithPrefix(''), isEmpty); - }); - }); + return true; + } + + @override + Future setInt(String key, Object value) async { + items[key] = value; + return true; + } + + @override + Future setString(String key, String value) async { + items[key] = value; + return true; + } + + @override + Future setStringList(String key, List value) async { + items[key] = value; + return true; + } } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value;