ReviewOS

also looking at this

craft-native/craft

refactor(android): one SharedPreferences helper, after the third caller turned out to be dead

#170
Merged chris wants to merge refactor/android-prefs into main
3 files +165 -113
packages/zig/src/android_prefs.zigadded+149-0
Changes to packages/zig/src/android_prefs.zig
@@ -0,0 +1,149 @@
1//! `SharedPreferences`, which three actions now reach for.
2//!
3//! The shim opens a preferences file by name in five places — the shared-item
4//! trio, the widget data, the voice actions — and each does the same four
5//! calls: `getSharedPreferences`, `edit`, `putString`/`remove`, `apply`. This
6//! is that sequence once.
7//!
8//! What it deliberately does *not* own is the file's name. Each caller decides
9//! that, because the names are the part that differs and the part that goes
10//! wrong: `craft_shared_<group>` is built from page text, `craft_widget_prefs`
11//! has to match what `CraftWidgetProvider` reads, and `craft_voice_actions` is
12//! read by nothing else at all. A helper that also chose the name would be
13//! four helpers wearing one signature.
14//!
15//! ## `apply`, not `commit`
16//!
17//! Every writer here uses `apply`: it hands the write to a background thread
18//! and returns void, so a failure is not observable at the call site. That is
19//! what the shim does, and it is why every one of these actions resolves with
20//! a promise about having *asked* rather than about having written.
21
22const std = ;
23const jni = ;
24
25const Jni = jni.Jni;
26const jobject = jni.jobject;
27
28/// `Context.MODE_PRIVATE`.
29///
30/// A compile-time constant in Java, so the shim's DEX holds the literal 0
31/// rather than a field read — which is why it is a literal here too.
32pub const mode_private: i32 = 0;
33
34/// `activity.getSharedPreferences(name, MODE_PRIVATE)`.
35///
36/// `name` is a slice rather than a literal because one caller builds it from
37/// page text, and it goes through `newStringUtf8` for the same reason.
38pub fn open(j: Jni, allocator: std.mem.Allocator, activity: jobject, name: []const u8) !jobject {
39 return j.callObjectMethodA(
40 activity,
41 try j.methodId(
42 try j.objectClass(activity),
43 "getSharedPreferences",
44 "(Ljava/lang/String;I)Landroid/content/SharedPreferences;",
45 ),
46 &.{ .{ .l = try j.newStringUtf8(allocator, name) }, .{ .i = mode_private } },
47 );
48}
49
50/// `prefs.edit()`.
51pub fn edit(j: Jni, prefs: jobject) !jobject {
52 return j.callObjectMethod(
53 prefs,
54 try j.methodId(
55 try j.objectClass(prefs),
56 "edit",
57 "()Landroid/content/SharedPreferences$Editor;",
58 ),
59 );
60}
61
62/// `editor.putString(key, value)`, whose return value is the editor again.
63pub fn putString(
64 j: Jni,
65 allocator: std.mem.Allocator,
66 editor: jobject,
67 key: []const u8,
68 value: []const u8,
69) !void {
70 try j.pushLocalFrame(4);
71 defer _ = j.popLocalFrame(null);
72
73 _ = try j.callObjectMethodA(
74 editor,
75 try j.methodId(
76 try j.objectClass(editor),
77 "putString",
78 "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;",
79 ),
80 &.{
81 .{ .l = try j.newStringUtf8(allocator, key) },
82 .{ .l = try j.newStringUtf8(allocator, value) },
83 },
84 );
85}
86
87/// `editor.remove(key)`.
88pub fn remove(j: Jni, allocator: std.mem.Allocator, editor: jobject, key: []const u8) !void {
89 try j.pushLocalFrame(4);
90 defer _ = j.popLocalFrame(null);
91
92 _ = try j.callObjectMethodA(
93 editor,
94 try j.methodId(
95 try j.objectClass(editor),
96 "remove",
97 "(Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;",
98 ),
99 &.{.{ .l = try j.newStringUtf8(allocator, key) }},
100 );
101}
102
103/// `editor.apply()`.
104pub fn apply(j: Jni, editor: jobject) !void {
105 try j.callVoidMethodA(
106 editor,
107 try j.methodId(try j.objectClass(editor), "apply", "()V"),
108 &.{},
109 );
110}
111
112/// `prefs.getString(key, null)`, null included.
113///
114/// The caller owns the returned bytes. Null means the key is absent, which is
115/// a different answer from an empty string and is kept as one.
116pub fn getString(
117 j: Jni,
118 allocator: std.mem.Allocator,
119 prefs: jobject,
120 key: []const u8,
121) !?[]u8 {
122 try j.pushLocalFrame(8);
123 defer _ = j.popLocalFrame(null);
124
125 const value = try j.callObjectMethodA(
126 prefs,
127 try j.methodId(
128 try j.objectClass(prefs),
129 "getString",
130 "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
131 ),
132 &.{ .{ .l = try j.newStringUtf8(allocator, key) }, .{ .l = null } },
133 );
134
135 if (value == null) return null;
136 return try j.stringToUtf8(allocator, value);
137}
138
139// =============================================================================
140// Tests
141// =============================================================================
142
143const testing = std.testing;
144
145test "MODE_PRIVATE is zero" {
146 // The shim's DEX holds this literal, so a field read here would be the
147 // slower way to get the same number and a chance to get a different one.
148 try testing.expectEqual(@as(i32, 0), mode_private);
149}
packages/zig/src/bridge_android_shareditem.zigmodified+9-77
Changes to packages/zig/src/bridge_android_shareditem.zig
@@ -25,6 +25,7 @@
2525const std = ;
2626const jni = ;
2727const bridge_error = ;
28const prefs_api = ;
2829
2930const Jni = jni.Jni;
3031const jobject = jni.jobject;
@@ -38,9 +39,6 @@ pub const A = struct {
3839pub const resolve_global = "_craftSharedKeychainResolve";
3940pub const reject_global = "_craftSharedKeychainReject";
4041
41/// `Context.MODE_PRIVATE`, a compile-time constant the shim's DEX holds as 0.
42const mode_private: i32 = 0;
43
4442const prefix = "craft_shared";
4543
4644/// `if (group.isNotEmpty()) "craft_shared_$group" else "craft_shared"`.
@@ -97,17 +95,7 @@ pub fn valuePayload(allocator: std.mem.Allocator, key: []const u8, value: ?[]con
9795fn openPrefs(j: Jni, allocator: std.mem.Allocator, activity: jobject, group: []const u8) !jobject {
9896 const name = try prefsName(allocator, group);
9997 defer allocator.free(name);
100
101 const activity_cls = try j.objectClass(activity);
102 return j.callObjectMethodA(
103 activity,
104 try j.methodId(
105 activity_cls,
106 "getSharedPreferences",
107 "(Ljava/lang/String;I)Landroid/content/SharedPreferences;",
108 ),
109 &.{ .{ .l = try j.newStringUtf8(allocator, name) }, .{ .i = mode_private } },
110 );
98 return prefs_api.open(j, allocator, activity, name);
11199}
112100
113101/// `prefs.edit().putString(key, value).apply()`.
@@ -122,27 +110,9 @@ pub fn set(
122110 try j.pushLocalFrame(16);
123111 defer _ = j.popLocalFrame(null);
124112
125 const prefs = try openPrefs(j, allocator, activity, group);
126 const editor = try editorFor(j, prefs);
127 const editor_cls = try j.objectClass(editor);
128
129 _ = try j.callObjectMethodA(
130 editor,
131 try j.methodId(
132 editor_cls,
133 "putString",
134 "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;",
135 ),
136 &.{
137 .{ .l = try j.newStringUtf8(allocator, key) },
138 .{ .l = try j.newStringUtf8(allocator, value) },
139 },
140 );
141
142 // `apply` rather than `commit`: it writes on a background thread and
143 // returns void, so a failure is not observable here — which is exactly
144 // what the shim's resolve already promises regardless.
145 try j.callVoidMethodA(editor, try j.methodId(editor_cls, "apply", "()V"), &.{});
113 const editor = try prefs_api.edit(j, try openPrefs(j, allocator, activity, group));
114 try prefs_api.putString(j, allocator, editor, key, value);
115 try prefs_api.apply(j, editor);
146116}
147117
148118/// `prefs.edit().remove(key).apply()`.
@@ -156,20 +126,9 @@ pub fn remove(
156126 try j.pushLocalFrame(16);
157127 defer _ = j.popLocalFrame(null);
158128
159 const prefs = try openPrefs(j, allocator, activity, group);
160 const editor = try editorFor(j, prefs);
161 const editor_cls = try j.objectClass(editor);
162
163 _ = try j.callObjectMethodA(
164 editor,
165 try j.methodId(
166 editor_cls,
167 "remove",
168 "(Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;",
169 ),
170 &.{.{ .l = try j.newStringUtf8(allocator, key) }},
171 );
172 try j.callVoidMethodA(editor, try j.methodId(editor_cls, "apply", "()V"), &.{});
129 const editor = try prefs_api.edit(j, try openPrefs(j, allocator, activity, group));
130 try prefs_api.remove(j, allocator, editor, key);
131 try prefs_api.apply(j, editor);
173132}
174133
175134/// `prefs.getString(key, null)`, null included.
@@ -187,29 +146,7 @@ pub fn get(
187146 defer _ = j.popLocalFrame(null);
188147
189148 const prefs = try openPrefs(j, allocator, activity, group);
190 const value = try j.callObjectMethodA(
191 prefs,
192 try j.methodId(
193 try j.objectClass(prefs),
194 "getString",
195 "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
196 ),
197 &.{ .{ .l = try j.newStringUtf8(allocator, key) }, .{ .l = null } },
198 );
199
200 if (value == null) return null;
201 return try j.stringToUtf8(allocator, value);
202}
203
204fn editorFor(j: Jni, prefs: jobject) !jobject {
205 return j.callObjectMethod(
206 prefs,
207 try j.methodId(
208 try j.objectClass(prefs),
209 "edit",
210 "()Landroid/content/SharedPreferences$Editor;",
211 ),
212 );
149 return prefs_api.getString(j, allocator, prefs, key);
213150}
214151
215152// =============================================================================
@@ -287,11 +224,6 @@ test "a key or value carrying a quote survives as JSON" {
287224 try testing.expect(parsed_success.value.object.get("success").?.bool);
288225}
289226
290test "MODE_PRIVATE is zero" {
291 // A compile-time constant in Java, so the shim's DEX holds the literal.
292 try testing.expectEqual(@as(i32, 0), mode_private);
293}
294
295227test "the actions and globals match the shim exactly" {
296228 try testing.expectEqualStrings("setSharedItem", A.set_shared_item);
297229 try testing.expectEqualStrings("getSharedItem", A.get_shared_item);
packages/zig/src/bridge_android_widgets.zigmodified+7-36
Changes to packages/zig/src/bridge_android_widgets.zig
@@ -34,6 +34,7 @@
3434
3535const std = ;
3636const jni = ;
37const prefs_api = ;
3738
3839const Jni = jni.Jni;
3940const jobject = jni.jobject;
@@ -50,9 +51,9 @@ pub const reject_global = "_craftWidgetReject";
5051pub const updated_result = "{\"updated\":true}";
5152pub const reloaded_result = "{\"reloaded\":true}";
5253
53/// `activity.getSharedPreferences("craft_widget_prefs", MODE_PRIVATE)`.
54/// `activity.getSharedPreferences("craft_widget_prefs", MODE_PRIVATE)` — the
55/// file `CraftWidgetProvider` reads back.
5456const prefs_name = "craft_widget_prefs";
55const mode_private: i32 = 0;
5657
5758/// The four fields, and the preference key each is written to.
5859///
@@ -106,45 +107,16 @@ pub fn writeUpdate(j: Jni, allocator: std.mem.Allocator, activity: jobject, upda
106107 try j.pushLocalFrame(16);
107108 defer _ = j.popLocalFrame(null);
108109
109 const prefs = try j.callObjectMethodA(
110 activity,
111 try j.methodId(
112 try j.objectClass(activity),
113 "getSharedPreferences",
114 "(Ljava/lang/String;I)Landroid/content/SharedPreferences;",
115 ),
116 &.{ .{ .l = try j.newStringUtf(prefs_name) }, .{ .i = mode_private } },
117 );
118
119 const editor = try j.callObjectMethod(
120 prefs,
121 try j.methodId(
122 try j.objectClass(prefs),
123 "edit",
124 "()Landroid/content/SharedPreferences$Editor;",
125 ),
126 );
127 const editor_cls = try j.objectClass(editor);
128 const put_string = try j.methodId(
129 editor_cls,
130 "putString",
131 "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;",
132 );
110 const prefs = try prefs_api.open(j, allocator, activity, prefs_name);
111 const editor = try prefs_api.edit(j, prefs);
133112
134113 inline for (fields, 0..) |field, i| {
135 if (update[i]) |text| {
136 try j.pushLocalFrame(4);
137 defer _ = j.popLocalFrame(null);
138 _ = try j.callObjectMethodA(editor, put_string, &.{
139 .{ .l = try j.newStringUtf(field.key) },
140 .{ .l = try j.newStringUtf8(allocator, text) },
141 });
142 }
114 if (update[i]) |text| try prefs_api.putString(j, allocator, editor, field.key, text);
143115 }
144116
145117 // `apply`, as the shim does — the write lands on a background thread and
146118 // the resolve is a promise about having asked.
147 try j.callVoidMethodA(editor, try j.methodId(editor_cls, "apply", "()V"), &.{});
119 try prefs_api.apply(j, editor);
148120}
149121
150122/// `Intent(action).setPackage(activity.packageName)`, then `sendBroadcast`.
@@ -314,7 +286,6 @@ test "the results are the shim's constants" {
314286
315287test "the preferences file is the one CraftWidgetProvider reads" {
316288 try testing.expectEqualStrings("craft_widget_prefs", prefs_name);
317 try testing.expectEqual(@as(i32, 0), mode_private);
318289}
319290
320291test "the actions and globals match the shim exactly" {