blob: d58cc32ed625c557ff35c4698842e5908d2caa85 [file] [log] [blame]
// Copyright 2019 The Chromium 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.SynchronousQueue;
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;
/**
* 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 SynchronousQueue<Runnable>());
}
@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)) {
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<String> 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 "getAll":
result.success(getAllPrefs());
return;
case "remove":
commitAsync(preferences.edit().remove(key), result);
break;
case "clear":
Set<String> keySet = getAllPrefs().keySet();
SharedPreferences.Editor clearEditor = preferences.edit();
for (String keyToDelete : keySet) {
clearEditor.remove(keyToDelete);
}
commitAsync(clearEditor, result);
break;
default:
result.notImplemented();
break;
}
} catch (IOException e) {
result.error("IOException encountered", call.method, e);
}
}
private void commitAsync(
final SharedPreferences.Editor editor, final MethodChannel.Result result) {
final Handler handler = new Handler(Looper.getMainLooper());
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<String> decodeList(String encodedList) throws IOException {
ObjectInputStream stream = null;
try {
stream = new ObjectInputStream(new ByteArrayInputStream(Base64.decode(encodedList, 0)));
return (List<String>) stream.readObject();
} catch (ClassNotFoundException e) {
throw new IOException(e);
} finally {
if (stream != null) {
stream.close();
}
}
}
private String encodeList(List<String> 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();
}
}
}
// Filter preferences to only those set by the flutter app.
private Map<String, Object> getAllPrefs() throws IOException {
Map<String, ?> allPrefs = preferences.getAll();
Map<String, Object> filteredPrefs = new HashMap<>();
for (String key : allPrefs.keySet()) {
if (key.startsWith("flutter.")) {
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<String> 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;
}
}