Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit fe9b559

Browse files
author
Chris Yang
authored
[shared_preferences] Support v2 android embedding. (#2162)
1 parent 28107c1 commit fe9b559

File tree

16 files changed

+345
-216
lines changed

16 files changed

+345
-216
lines changed

packages/shared_preferences/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.5.4
2+
3+
* Support the v2 Android embedding.
4+
* Update to AndroidX.
5+
* Migrate to using the new e2e test binding.
6+
17
## 0.5.3+5
28

39
* Define clang module for iOS.

packages/shared_preferences/android/build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,29 @@ android {
5353
disable 'InvalidPackage'
5454
}
5555
}
56+
57+
// TODO(cyanglaz): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348
58+
afterEvaluate {
59+
def containsEmbeddingDependencies = false
60+
for (def configuration : configurations.all) {
61+
for (def dependency : configuration.dependencies) {
62+
if (dependency.group == 'io.flutter' &&
63+
dependency.name.startsWith('flutter_embedding') &&
64+
dependency.isTransitive())
65+
{
66+
containsEmbeddingDependencies = true
67+
break
68+
}
69+
}
70+
}
71+
if (!containsEmbeddingDependencies) {
72+
android {
73+
dependencies {
74+
def lifecycle_version = "1.1.1"
75+
api "android.arch.lifecycle:runtime:$lifecycle_version"
76+
api "android.arch.lifecycle:common:$lifecycle_version"
77+
api "android.arch.lifecycle:common-java8:$lifecycle_version"
78+
}
79+
}
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.sharedpreferences;
6+
7+
import android.content.Context;
8+
import android.content.SharedPreferences;
9+
import android.os.AsyncTask;
10+
import android.util.Base64;
11+
import io.flutter.plugin.common.MethodCall;
12+
import io.flutter.plugin.common.MethodChannel;
13+
import java.io.ByteArrayInputStream;
14+
import java.io.ByteArrayOutputStream;
15+
import java.io.IOException;
16+
import java.io.ObjectInputStream;
17+
import java.io.ObjectOutputStream;
18+
import java.math.BigInteger;
19+
import java.util.ArrayList;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Set;
24+
25+
/**
26+
* Implementation of the {@link MethodChannel.MethodCallHandler} for the plugin. It is also
27+
* responsible of managing the {@link android.content.SharedPreferences}.
28+
*/
29+
class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
30+
31+
private static final String SHARED_PREFERENCES_NAME = "FlutterSharedPreferences";
32+
33+
// Fun fact: The following is a base64 encoding of the string "This is the prefix for a list."
34+
private static final String LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu";
35+
private static final String BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy";
36+
private static final String DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu";
37+
38+
private final android.content.SharedPreferences preferences;
39+
40+
/**
41+
* Constructs a {@link MethodCallHandlerImpl} instance. Creates a {@link
42+
* android.content.SharedPreferences} based on the {@code context}.
43+
*/
44+
MethodCallHandlerImpl(Context context) {
45+
preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
46+
}
47+
48+
@Override
49+
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
50+
String key = call.argument("key");
51+
try {
52+
switch (call.method) {
53+
case "setBool":
54+
commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument("value")), result);
55+
break;
56+
case "setDouble":
57+
double doubleValue = ((Number) call.argument("value")).doubleValue();
58+
String doubleValueStr = Double.toString(doubleValue);
59+
commitAsync(preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr), result);
60+
break;
61+
case "setInt":
62+
Number number = call.argument("value");
63+
if (number instanceof BigInteger) {
64+
BigInteger integerValue = (BigInteger) number;
65+
commitAsync(
66+
preferences
67+
.edit()
68+
.putString(
69+
key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX)),
70+
result);
71+
} else {
72+
commitAsync(preferences.edit().putLong(key, number.longValue()), result);
73+
}
74+
break;
75+
case "setString":
76+
String value = (String) call.argument("value");
77+
if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) {
78+
result.error(
79+
"StorageError",
80+
"This string cannot be stored as it clashes with special identifier prefixes.",
81+
null);
82+
return;
83+
}
84+
commitAsync(preferences.edit().putString(key, value), result);
85+
break;
86+
case "setStringList":
87+
List<String> list = call.argument("value");
88+
commitAsync(
89+
preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result);
90+
break;
91+
case "commit":
92+
// We've been committing the whole time.
93+
result.success(true);
94+
break;
95+
case "getAll":
96+
result.success(getAllPrefs());
97+
return;
98+
case "remove":
99+
commitAsync(preferences.edit().remove(key), result);
100+
break;
101+
case "clear":
102+
Set<String> keySet = getAllPrefs().keySet();
103+
SharedPreferences.Editor clearEditor = preferences.edit();
104+
for (String keyToDelete : keySet) {
105+
clearEditor.remove(keyToDelete);
106+
}
107+
commitAsync(clearEditor, result);
108+
break;
109+
default:
110+
result.notImplemented();
111+
break;
112+
}
113+
} catch (IOException e) {
114+
result.error("IOException encountered", call.method, e);
115+
}
116+
}
117+
118+
private void commitAsync(
119+
final SharedPreferences.Editor editor, final MethodChannel.Result result) {
120+
new AsyncTask<Void, Void, Boolean>() {
121+
@Override
122+
protected Boolean doInBackground(Void... voids) {
123+
return editor.commit();
124+
}
125+
126+
@Override
127+
protected void onPostExecute(Boolean value) {
128+
result.success(value);
129+
}
130+
}.execute();
131+
}
132+
133+
private List<String> decodeList(String encodedList) throws IOException {
134+
ObjectInputStream stream = null;
135+
try {
136+
stream = new ObjectInputStream(new ByteArrayInputStream(Base64.decode(encodedList, 0)));
137+
return (List<String>) stream.readObject();
138+
} catch (ClassNotFoundException e) {
139+
throw new IOException(e);
140+
} finally {
141+
if (stream != null) {
142+
stream.close();
143+
}
144+
}
145+
}
146+
147+
private String encodeList(List<String> list) throws IOException {
148+
ObjectOutputStream stream = null;
149+
try {
150+
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
151+
stream = new ObjectOutputStream(byteStream);
152+
stream.writeObject(list);
153+
stream.flush();
154+
return Base64.encodeToString(byteStream.toByteArray(), 0);
155+
} finally {
156+
if (stream != null) {
157+
stream.close();
158+
}
159+
}
160+
}
161+
162+
// Filter preferences to only those set by the flutter app.
163+
private Map<String, Object> getAllPrefs() throws IOException {
164+
Map<String, ?> allPrefs = preferences.getAll();
165+
Map<String, Object> filteredPrefs = new HashMap<>();
166+
for (String key : allPrefs.keySet()) {
167+
if (key.startsWith("flutter.")) {
168+
Object value = allPrefs.get(key);
169+
if (value instanceof String) {
170+
String stringValue = (String) value;
171+
if (stringValue.startsWith(LIST_IDENTIFIER)) {
172+
value = decodeList(stringValue.substring(LIST_IDENTIFIER.length()));
173+
} else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) {
174+
String encoded = stringValue.substring(BIG_INTEGER_PREFIX.length());
175+
value = new BigInteger(encoded, Character.MAX_RADIX);
176+
} else if (stringValue.startsWith(DOUBLE_PREFIX)) {
177+
String doubleStr = stringValue.substring(DOUBLE_PREFIX.length());
178+
value = Double.valueOf(doubleStr);
179+
}
180+
} else if (value instanceof Set) {
181+
// This only happens for previous usage of setStringSet. The app expects a list.
182+
List<String> listValue = new ArrayList<>((Set) value);
183+
// Let's migrate the value too while we are at it.
184+
boolean success =
185+
preferences
186+
.edit()
187+
.remove(key)
188+
.putString(key, LIST_IDENTIFIER + encodeList(listValue))
189+
.commit();
190+
if (!success) {
191+
// If we are unable to migrate the existing preferences, it means we potentially lost them.
192+
// In this case, an error from getAllPrefs() is appropriate since it will alert the app during plugin initialization.
193+
throw new IOException("Could not migrate set to list");
194+
}
195+
value = listValue;
196+
}
197+
filteredPrefs.put(key, value);
198+
}
199+
}
200+
return filteredPrefs;
201+
}
202+
}

0 commit comments

Comments
 (0)