| // 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.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 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.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** SharedPreferencesPlugin */ |
| 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( |
| @NonNull io.flutter.plugin.common.PluginRegistry.Registrar registrar) { |
| final SharedPreferencesPlugin plugin = new SharedPreferencesPlugin(); |
| 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(@NonNull FlutterPlugin.FlutterPluginBinding binding) { |
| setUp(binding.getBinaryMessenger(), binding.getApplicationContext()); |
| } |
| |
| @Override |
| public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { |
| SharedPreferencesApi.setup(binding.getBinaryMessenger(), null); |
| } |
| |
| @Override |
| public @NonNull Boolean setBool(@NonNull String key, @NonNull Boolean value) { |
| return preferences.edit().putBoolean(key, value).commit(); |
| } |
| |
| @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<String> value) |
| throws RuntimeException { |
| return preferences.edit().putString(key, LIST_IDENTIFIER + listEncoder.encode(value)).commit(); |
| } |
| |
| @Override |
| public @NonNull Map<String, Object> getAll( |
| @NonNull String prefix, @Nullable List<String> allowList) throws RuntimeException { |
| final Set<String> allowSet = allowList == null ? null : new HashSet<>(allowList); |
| return getAllPrefs(prefix, allowSet); |
| } |
| |
| @Override |
| public @NonNull Boolean clear(@NonNull String prefix, @Nullable List<String> allowList) |
| throws RuntimeException { |
| SharedPreferences.Editor clearEditor = preferences.edit(); |
| Map<String, ?> allPrefs = preferences.getAll(); |
| ArrayList<String> 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]. |
| @SuppressWarnings("unchecked") |
| private @NonNull Map<String, Object> getAllPrefs( |
| @NonNull String prefix, @Nullable Set<String> allowList) throws RuntimeException { |
| Map<String, ?> allPrefs = preferences.getAll(); |
| Map<String, Object> 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))); |
| } |
| } |
| |
| 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<String> listValue = new ArrayList<>((Set<String>) 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<String> 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<String> decode(@NonNull String listString) throws RuntimeException { |
| try { |
| ObjectInputStream stream = |
| new ObjectInputStream(new ByteArrayInputStream(Base64.decode(listString, 0))); |
| return (List<String>) stream.readObject(); |
| } catch (IOException | ClassNotFoundException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| } |