blob: aecc43991cc3f7e588f7322afa6288f2229194c9 [file] [log] [blame]
// 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);
}
}
}
}