Skip to content

Commit 9b03e4e

Browse files
committed
Add RootArgs class and enhance RootOptions and RootThread for custom argument handling
1 parent 3802ad9 commit 9b03e4e

5 files changed

Lines changed: 288 additions & 12 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package dev.mmrlx.thread;
2+
3+
import androidx.annotation.NonNull;
4+
import androidx.annotation.Nullable;
5+
6+
import java.io.Serial;
7+
import java.io.Serializable;
8+
import java.util.Collections;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
import java.util.Set;
12+
13+
/**
14+
* A typed, serializable container for custom arguments that are passed alongside a
15+
* {@link RootCallable} and survive the cross-process boundary.
16+
*
17+
* <p>All values must be serializable by Kryo (e.g. primitives, {@link String},
18+
* {@link java.io.Serializable} objects, or {@link android.os.Parcelable} types).
19+
*
20+
* <h3>Example</h3>
21+
* <pre>{@code
22+
* RootArgs args = RootArgs.of("path", "/data/local/tmp/test.txt")
23+
* .with("overwrite", true);
24+
*
25+
* RootThread.submit(options -> {
26+
* String path = options.getArgs().get("path");
27+
* boolean overwrite = options.getArgs().get("overwrite", false);
28+
* // …
29+
* return null;
30+
* }, args);
31+
* }</pre>
32+
*/
33+
public final class RootArgs implements Serializable {
34+
35+
@Serial
36+
private static final long serialVersionUID = 1L;
37+
38+
/** Shared empty instance; avoids allocations when no args are needed. */
39+
public static final RootArgs EMPTY = new RootArgs(Collections.emptyMap());
40+
41+
private final Map<String, Object> mArgs;
42+
43+
private RootArgs(@NonNull Map<String, Object> args) {
44+
mArgs = args;
45+
}
46+
47+
/**
48+
* Creates a new {@code RootArgs} with a single key-value pair.
49+
*
50+
* @param key The argument name.
51+
* @param value The argument value; must be Kryo-serializable.
52+
*/
53+
@NonNull
54+
public static RootArgs of(@NonNull String key, @Nullable Object value) {
55+
Map<String, Object> map = new HashMap<>();
56+
map.put(key, value);
57+
return new RootArgs(map);
58+
}
59+
60+
/**
61+
* Creates a new {@code RootArgs} from an existing map.
62+
*
63+
* @param args Key-value pairs; the map is copied defensively.
64+
*/
65+
@NonNull
66+
public static RootArgs of(@NonNull Map<String, ?> args) {
67+
return new RootArgs(new HashMap<>(args));
68+
}
69+
70+
/**
71+
* Returns a new {@code RootArgs} with the given key-value pair added (or replaced).
72+
*/
73+
@NonNull
74+
public RootArgs with(@NonNull String key, @Nullable Object value) {
75+
Map<String, Object> copy = new HashMap<>(mArgs);
76+
copy.put(key, value);
77+
return new RootArgs(copy);
78+
}
79+
80+
/**
81+
* Returns the value associated with {@code key}, or {@code null} if absent.
82+
*
83+
* @param key The argument name.
84+
* @param <T> The expected type.
85+
*/
86+
@Nullable
87+
@SuppressWarnings("unchecked")
88+
public <T> T get(@NonNull String key) {
89+
return (T) mArgs.get(key);
90+
}
91+
92+
/**
93+
* Returns the value associated with {@code key}, or {@code defaultValue} if absent.
94+
*
95+
* @param key The argument name.
96+
* @param defaultValue Fallback if the key is not present.
97+
* @param <T> The expected type.
98+
*/
99+
@SuppressWarnings("unchecked")
100+
public <T> T get(@NonNull String key, @NonNull T defaultValue) {
101+
Object value = mArgs.get(key);
102+
return value != null ? (T) value : defaultValue;
103+
}
104+
105+
/**
106+
* Returns {@code true} if an argument with the given key exists.
107+
*/
108+
public boolean has(@NonNull String key) {
109+
return mArgs.containsKey(key);
110+
}
111+
112+
/**
113+
* Returns an unmodifiable view of all argument keys.
114+
*/
115+
@NonNull
116+
public Set<String> keys() {
117+
return Collections.unmodifiableSet(mArgs.keySet());
118+
}
119+
120+
/**
121+
* Returns {@code true} if no arguments are stored.
122+
*/
123+
public boolean isEmpty() {
124+
return mArgs.isEmpty();
125+
}
126+
127+
@NonNull
128+
@Override
129+
public String toString() {
130+
return "RootArgs" + mArgs;
131+
}
132+
}

thread/src/main/java/dev/mmrlx/thread/RootOptions.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@
1010
* Represents the configuration options for root-related operations within the threading framework.
1111
* <p>
1212
* This class encapsulates necessary dependencies and environment settings, such as the
13-
* {@link Context}, required for executing tasks that interact with the system at a root level.
13+
* {@link Context} and any custom {@link RootArgs}, required for executing tasks that interact
14+
* with the system at a root level.
1415
*/
1516
public class RootOptions {
1617
@NonNull
1718
private final Context mContext;
1819

19-
RootOptions(@NonNull Context context) {
20+
@NonNull
21+
private final RootArgs mArgs;
22+
23+
public RootOptions(@NonNull Context context, @NonNull RootArgs args) {
2024
mContext = context;
25+
mArgs = args;
2126
}
2227

2328
/**
@@ -29,4 +34,13 @@ public class RootOptions {
2934
public Context getContext() {
3035
return mContext;
3136
}
37+
38+
/**
39+
* Returns the custom arguments that were supplied when the callable was submitted.
40+
* Returns {@link RootArgs#EMPTY} if no arguments were provided.
41+
*/
42+
@NonNull
43+
public RootArgs getArgs() {
44+
return mArgs;
45+
}
3246
}

thread/src/main/java/dev/mmrlx/thread/RootThread.java

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.objenesis.strategy.StdInstantiatorStrategy;
2727

2828
import java.io.IOException;
29+
import java.io.Serial;
30+
import java.io.Serializable;
2931
import java.util.Objects;
3032
import java.util.concurrent.CompletableFuture;
3133
import java.util.concurrent.ExecutionException;
@@ -159,7 +161,24 @@ private static ClassResolver getAppClassResolver() {
159161
*/
160162
@NonNull
161163
public static <T> Future<T> submit(@NonNull RootCallable<T> callable) {
162-
return sExecutor.submit(() -> executeSync(callable));
164+
return submit(callable, RootArgs.EMPTY);
165+
}
166+
167+
/**
168+
* Submits {@code callable} together with {@code args} to the root process and returns a
169+
* {@link Future} that resolves with the callable's return value.
170+
*
171+
* <p>The supplied {@code args} are serialized alongside the callable and are available
172+
* inside the root process via {@link RootOptions#getArgs()}.
173+
*
174+
* @param callable The closure to execute in the root process.
175+
* @param args Custom arguments to pass to the callable; must be Kryo-serializable.
176+
* @param <T> Return type of the callable.
177+
* @return A {@link Future} representing the pending result.
178+
*/
179+
@NonNull
180+
public static <T> Future<T> submit(@NonNull RootCallable<T> callable, @NonNull RootArgs args) {
181+
return sExecutor.submit(() -> executeSync(callable, args));
163182
}
164183

165184
/**
@@ -175,8 +194,26 @@ public static <T> Future<T> submit(@NonNull RootCallable<T> callable) {
175194
@Nullable
176195
public static <T> T executeBlocking(@NonNull RootCallable<T> callable)
177196
throws IOException, InterruptedException {
197+
return executeBlocking(callable, RootArgs.EMPTY);
198+
}
199+
200+
/**
201+
* Submits {@code callable} together with {@code args} to the root process and blocks the
202+
* calling thread until the result is available. Must NOT be called on the main thread.
203+
*
204+
* @param callable The closure to execute in the root process.
205+
* @param args Custom arguments available in the root process via
206+
* {@link RootOptions#getArgs()}.
207+
* @param <T> Return type of the callable.
208+
* @return The value returned by the callable.
209+
* @throws IOException If IPC serialisation, transport, or the remote call fails.
210+
* @throws InterruptedException If the calling thread is interrupted while waiting.
211+
*/
212+
@Nullable
213+
public static <T> T executeBlocking(@NonNull RootCallable<T> callable, @NonNull RootArgs args)
214+
throws IOException, InterruptedException {
178215
try {
179-
return submit(callable).get();
216+
return submit(callable, args).get();
180217
} catch (ExecutionException e) {
181218
Throwable cause = e.getCause();
182219
if (cause instanceof IOException) throw (IOException) cause;
@@ -202,9 +239,34 @@ public static <T> T executeBlocking(
202239
@NonNull RootCallable<T> callable,
203240
long timeout,
204241
@NonNull TimeUnit unit
242+
) throws IOException, InterruptedException, TimeoutException {
243+
return executeBlocking(callable, RootArgs.EMPTY, timeout, unit);
244+
}
245+
246+
/**
247+
* Submits {@code callable} together with {@code args} to the root process and blocks the
248+
* calling thread for at most {@code timeout} {@code unit}s.
249+
*
250+
* @param callable The closure to execute in the root process.
251+
* @param args Custom arguments available in the root process via
252+
* {@link RootOptions#getArgs()}.
253+
* @param timeout Maximum time to wait.
254+
* @param unit Time unit for {@code timeout}.
255+
* @param <T> Return type of the callable.
256+
* @return The value returned by the callable.
257+
* @throws IOException If IPC serialisation, transport, or the remote call fails.
258+
* @throws InterruptedException If the calling thread is interrupted while waiting.
259+
* @throws TimeoutException If the operation does not complete within the timeout.
260+
*/
261+
@Nullable
262+
public static <T> T executeBlocking(
263+
@NonNull RootCallable<T> callable,
264+
@NonNull RootArgs args,
265+
long timeout,
266+
@NonNull TimeUnit unit
205267
) throws IOException, InterruptedException, TimeoutException {
206268
try {
207-
return submit(callable).get(timeout, unit);
269+
return submit(callable, args).get(timeout, unit);
208270
} catch (ExecutionException e) {
209271
Throwable cause = e.getCause();
210272
if (cause instanceof IOException) throw (IOException) cause;
@@ -219,9 +281,21 @@ public static <T> T executeBlocking(
219281
*
220282
* @throws IOException On any serialization, IPC, or type-mismatch error.
221283
*/
222-
@SuppressWarnings("unchecked")
223284
@Nullable
224285
static <T> T executeSync(@NonNull RootCallable<T> callable) throws IOException {
286+
return executeSync(callable, RootArgs.EMPTY);
287+
}
288+
289+
/**
290+
* Core synchronous IPC routine with custom arguments. Wraps {@code callable} and
291+
* {@code args} in a {@link CallableEnvelope}, serializes the envelope into a pipe,
292+
* hands the read-end to the root service, then reads and deserializes the result.
293+
*
294+
* @throws IOException On any serialization, IPC, or type-mismatch error.
295+
*/
296+
@SuppressWarnings("unchecked")
297+
@Nullable
298+
static <T> T executeSync(@NonNull RootCallable<T> callable, @NonNull RootArgs args) throws IOException {
225299
IRootThread svc;
226300
try {
227301
svc = sRootServiceFuture.get();
@@ -245,7 +319,7 @@ static <T> T executeSync(@NonNull RootCallable<T> callable) throws IOException {
245319
new ParcelFileDescriptor.AutoCloseOutputStream(callableWrite);
246320
Output output = new Output(fos)) {
247321
KryoManager kryo = buildKryo(null);
248-
kryo.writeClassAndObject(output, callable);
322+
kryo.writeClassAndObject(output, new CallableEnvelope(callable, args));
249323
output.flush();
250324
}
251325

@@ -387,7 +461,23 @@ public Parcelable read(Kryo kryo, Input input, Class<? extends Parcelable> type)
387461
}
388462

389463
public static final Companion Companion = new Companion();
464+
390465
public static final class Companion {
391-
private Companion() {}
466+
private Companion() {
467+
}
468+
}
469+
470+
/**
471+
* Serialization envelope that bundles a {@link RootCallable} with its {@link RootArgs}
472+
* so both travel through the same Kryo-serialized pipe in a single pass.
473+
*/
474+
record CallableEnvelope(RootCallable<?> callable, RootArgs args) implements Serializable {
475+
@Serial
476+
private static final long serialVersionUID = 1L;
477+
478+
CallableEnvelope(@NonNull RootCallable<?> callable, @NonNull RootArgs args) {
479+
this.callable = callable;
480+
this.args = args;
481+
}
392482
}
393483
}

thread/src/main/java/dev/mmrlx/thread/RootThreadService.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,19 @@ public void execute(
9191
RootThread.KryoManager kryo = RootThread.buildKryo(
9292
getAppClassResolver(),
9393
RootThreadBinder.class.getClassLoader());
94-
RootCallable<?> callable = (RootCallable<?>) kryo.readClassAndObject(input);
94+
RootThread.CallableEnvelope envelope =
95+
(RootThread.CallableEnvelope) kryo.readClassAndObject(input);
9596

9697
try {
97-
RootOptions options = new RootOptions(mContext);
98-
result = callable.call(options);
98+
RootOptions options = new RootOptions(mContext, envelope.args());
99+
result = envelope.callable().call(options);
99100
} catch (Throwable t) {
100101
Log.e(TAG, "Root callable threw", t);
101102
result = t;
102103
}
103104

104105
} catch (Exception e) {
105-
Log.e(TAG, "Failed to deserialise callable", e);
106+
Log.e(TAG, "Failed to deserialize callable", e);
106107
result = new IOException("Deserialisation failed in root process", e);
107108
}
108109

0 commit comments

Comments
 (0)