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}