diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index b4ef17c76bf0..702f103174ba 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.27 + +* Converts legacy backend to Kotlin. + ## 2.4.26 * Updates internal implementation to use Kotlin Pigeon. 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 deleted file mode 100644 index 5a99d5f319b4..000000000000 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright 2013 The Flutter Authors -// 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.util.Base64; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -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.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 final SharedPreferencesListEncoder listEncoder; - - public LegacySharedPreferencesPlugin() { - this(new ListEncoder()); - } - - @VisibleForTesting - LegacySharedPreferencesPlugin(@NonNull SharedPreferencesListEncoder listEncoder) { - this.listEncoder = listEncoder; - } - - private void setUp(@NonNull BinaryMessenger messenger, @NonNull Context context) { - preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); - try { - SharedPreferencesApi.Companion.setUp(messenger, this); - } catch (Exception ex) { - Log.e(TAG, "Received exception while setting up SharedPreferencesPlugin", ex); - } - } - - @Override - public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { - setUp(binding.getBinaryMessenger(), binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { - SharedPreferencesApi.Companion.setUp(binding.getBinaryMessenger(), null); - } - - @Override - public boolean setBool(@NonNull String key, boolean value) { - return preferences.edit().putBoolean(key, value).commit(); - } - - @Override - public 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 boolean setInt(@NonNull String key, long value) { - return preferences.edit().putLong(key, value).commit(); - } - - @Override - public boolean setDouble(@NonNull String key, double value) { - String doubleValueStr = Double.toString(value); - return preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit(); - } - - @Override - public boolean remove(@NonNull String key) { - return preferences.edit().remove(key).commit(); - } - - @Override - public 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 boolean setDeprecatedStringList(@NonNull String key, @NonNull List value) - throws RuntimeException { - return preferences.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)).commit(); - } - - @Override - public @NonNull Map getAll( - @NonNull String prefix, @Nullable List allowList) throws RuntimeException { - final Set allowSet = allowList == null ? null : new HashSet<>(allowList); - return getAllPrefs(prefix, allowSet); - } - - @Override - public boolean clear(@NonNull String prefix, @Nullable List allowList) - throws RuntimeException { - SharedPreferences.Editor clearEditor = preferences.edit(); - Map allPrefs = preferences.getAll(); - ArrayList filteredPrefs = new ArrayList<>(); - for (String key : allPrefs.keySet()) { - if (key.startsWith(prefix) && (allowList == null || allowList.contains(key))) { - 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. - // Optionally filtered also to only those items in the optional [allowList]. - 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, Objects.requireNonNull(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)) { - // 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 - 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 StringListObjectInputStream(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/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt new file mode 100644 index 000000000000..a5f2757bf969 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.kt @@ -0,0 +1,182 @@ +// Copyright 2013 The Flutter Authors +// 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.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import android.util.Base64 +import android.util.Log +import androidx.annotation.VisibleForTesting +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.plugin.common.BinaryMessenger +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.math.BigInteger + +/** LegacySharedPreferencesPlugin */ +@SuppressLint("UseKtx") +class LegacySharedPreferencesPlugin +@VisibleForTesting +internal constructor(private val listEncoder: SharedPreferencesListEncoder) : + FlutterPlugin, SharedPreferencesApi { + private lateinit var preferences: SharedPreferences + + constructor() : this(ListEncoder()) + + private fun setUp(messenger: BinaryMessenger, context: Context) { + preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + try { + SharedPreferencesApi.setUp(messenger, this) + } catch (ex: Exception) { + Log.e(TAG, "Received exception while setting up SharedPreferencesPlugin", ex) + } + } + + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + setUp(binding.binaryMessenger, binding.applicationContext) + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { + SharedPreferencesApi.setUp(binding.binaryMessenger, null) + } + + override fun setBool(key: String, value: Boolean): Boolean { + return preferences.edit().putBoolean(key, value).commit() + } + + override fun setString(key: String, value: String): Boolean { + // TODO (tarrinneal): Move this string prefix checking logic to Dart code and make it an + // Argument Error. + if (value.startsWith(LIST_PREFIX) || + value.startsWith(BIG_INTEGER_PREFIX) || + value.startsWith(DOUBLE_PREFIX)) { + throw RuntimeException( + "StorageError: This string cannot be stored as it clashes with special identifier" + + " prefixes") + } + return preferences.edit().putString(key, value).commit() + } + + override fun setInt(key: String, value: Long): Boolean { + return preferences.edit().putLong(key, value).commit() + } + + override fun setDouble(key: String, value: Double): Boolean { + val doubleValueStr = value.toString() + return preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit() + } + + override fun remove(key: String): Boolean { + return preferences.edit().remove(key).commit() + } + + override fun setEncodedStringList(key: String, value: String): Boolean { + return preferences.edit().putString(key, value).commit() + } + + // Deprecated, for testing purposes only. + @Deprecated("") + override fun setDeprecatedStringList(key: String, value: List): Boolean { + return preferences.edit().putString(key, LIST_PREFIX + listEncoder.encode(value)).commit() + } + + override fun getAll(prefix: String, allowList: List?): Map { + return getAllPrefs(prefix, allowList?.toSet()) + } + + override fun clear(prefix: String, allowList: List?): Boolean { + val clearEditor = preferences.edit() + val allowSet = allowList?.toSet() + preferences.all.keys + .filter { key -> key.startsWith(prefix) && (allowSet == null || allowSet.contains(key)) } + .forEach { key -> clearEditor.remove(key) } + return clearEditor.commit() + } + + // Gets all shared preferences, filtered to only those set with the given prefix. + // Optionally filtered also to only those items in + private fun getAllPrefs(prefix: String, allowList: Set?): Map { + return buildMap { + preferences.all.forEach { (key, value) -> + if (key.startsWith(prefix) && + value != null && + (allowList == null || allowList.contains(key))) { + put(key, transformPref(key, value)) + } + } + } + } + + private fun transformPref(key: String, value: Any): Any { + if (value is String) { + if (value.startsWith(LIST_PREFIX)) { + // 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(BIG_INTEGER_PREFIX)) { + // TODO (tarrinneal): Remove all BigInt code. + // https://github.com/flutter/flutter/issues/124420 + val encoded: String = value.substring(BIG_INTEGER_PREFIX.length) + return BigInteger(encoded, Character.MAX_RADIX) + } else if (value.startsWith(DOUBLE_PREFIX)) { + val doubleStr: String = value.substring(DOUBLE_PREFIX.length) + return doubleStr.toDouble() + } + } else if (value is 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. + @Suppress("UNCHECKED_CAST") val listValue = (value as Set).toList() + // Let's migrate the value too while we are at it. + preferences + .edit() + .remove(key) + .putString(key, LIST_PREFIX + listEncoder.encode(listValue)) + .apply() + + return listValue + } + return value + } + + internal class ListEncoder : SharedPreferencesListEncoder { + override fun encode(list: List): String { + try { + val byteStream = ByteArrayOutputStream() + val stream = ObjectOutputStream(byteStream) + stream.writeObject(list) + stream.flush() + return Base64.encodeToString(byteStream.toByteArray(), 0) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun decode(listString: String): List { + try { + val stream: ObjectInputStream = + StringListObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0))) + @Suppress("UNCHECKED_CAST") return stream.readObject() as List + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: ClassNotFoundException) { + throw RuntimeException(e) + } + } + } + + companion object { + private const val BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy" + } +} 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/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt similarity index 59% rename from packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.java rename to packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt index f49cab3670b0..8b768737ddde 100644 --- 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/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesListEncoder.kt @@ -1,22 +1,16 @@ // Copyright 2013 The Flutter Authors // 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; +package io.flutter.plugins.sharedpreferences /** * An interface used to provide conversion logic between List and String for * SharedPreferencesPlugin. */ -public interface SharedPreferencesListEncoder { +interface SharedPreferencesListEncoder { /** Converts list to String for storing in shared preferences. */ - @NonNull - String encode(@NonNull List list); + fun encode(list: List): String /** Converts stored String representing List to List. */ - @NonNull - List decode(@NonNull String listString); + fun decode(listString: String): List } 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 dc54cb39c067..31e9f236f4ac 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 @@ -31,11 +31,10 @@ 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. +// All identifiers must match 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 JSON_LIST_PREFIX = "$LIST_PREFIX!" const val DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu" /*package private*/ val Context.sharedPreferencesDataStore: DataStore by 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 5e95d637ae2e..c9e1c8344367 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 @@ -39,6 +39,7 @@ public void before() { Context context = Mockito.mock(Context.class); SharedPreferences sharedPrefs = new FakeSharedPreferences(); + mockMessenger = Mockito.mock(BinaryMessenger.class); flutterPluginBinding = Mockito.mock(FlutterPlugin.FlutterPluginBinding.class); Mockito.when(flutterPluginBinding.getBinaryMessenger()).thenReturn(mockMessenger); diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index 9dabd4fa232b..8f152209487a 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.26 +version: 2.4.27 environment: sdk: ^3.12.0