Update shared Gaia workflows and Sargo development tooling

Fix camera switching and Contacts/SMS activity routing across shared hosts.
Integrate Marketplace installation and native standalone recovery tools,
Bluetooth runtime packages and trusted certificates.

Update English/French READMEs and the shared changelog with tested scope
and remaining radio/audio/Bluetooth limitations. Preview17 remains the
existing APK; no new native image is released.

Validation: shared JavaScript and native Python tests; Android packaging
and payload parity from an isolated publication checkout.
This commit is contained in:
OverSu 2026-10-08 14:27:17 +02:00
commit 0041cd9660
101 changed files with 3883 additions and 335 deletions

View file

@ -14,11 +14,13 @@ import java.util.zip.*;
final class GaiaServer implements Closeable {
static final int PORT = 18765;
final InstalledApps installed;
private final ZipFile zip;
private final ServerSocket socket;
private final ExecutorService workers = Executors.newFixedThreadPool(4);
GaiaServer(Context context) throws IOException {
try {installed=new InstalledApps(context);}catch(Exception e){throw new IOException(e);}
File archive = new File(context.getCacheDir(), "gaia.zip");
// Atomic refresh on every process start; the APK is the source of truth.
File temp = new File(context.getCacheDir(), "gaia.zip.tmp");
@ -94,7 +96,7 @@ final class GaiaServer implements Closeable {
}
String app = host.substring(0, host.indexOf('.'));
if (app.equals("theme")) app = "default_theme";
if (zip.getEntry(app + "/manifest.webapp") == null) {
if (!InstalledApps.valid(app) && zip.getEntry(app + "/manifest.webapp") == null) {
error(out, 404);
return;
}
@ -109,6 +111,16 @@ final class GaiaServer implements Closeable {
return;
}
if (path.endsWith("/")) path += "index.html";
if (InstalledApps.valid(app)) {
byte[] body=installed.read(app,path.substring(1));
if(body==null){error(out,404);return;}
String ext=path.substring(path.lastIndexOf('.')+1).toLowerCase(Locale.ROOT);
String mime=MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
if(ext.equals("js"))mime="text/javascript";
if(ext.equals("webapp"))mime="application/json";
if(mime==null)mime="application/octet-stream";
out.write(("HTTP/1.1 200 OK\r\nContent-Type: "+mime+"\r\nContent-Length: "+body.length+"\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-store\r\nAccess-Control-Allow-Origin: *\r\nContent-Security-Policy: object-src 'none'; frame-ancestors http://*.localhost:"+PORT+"\r\n\r\n").getBytes(StandardCharsets.US_ASCII));out.write(body);return;
}
String key = path.startsWith("/_vulpes/") ? path.substring(1) : app + path;
ZipEntry entry = zip.getEntry(key);
if (entry == null && path.startsWith("/shared/")) entry = zip.getEntry(

View file

@ -0,0 +1,37 @@
package org.vulpes_os.preview;
import android.content.Context;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import org.json.*;
final class InstalledApps {
private final File root;
private final Map<String,JSONObject> items = new HashMap<>();
InstalledApps(Context context) throws Exception {
root=new File(context.getFilesDir(),"installed-apps");root.mkdirs();
File[] files=root.listFiles((d,n)->n.matches("user-[a-f0-9]{24}\\.json"));
if(files!=null)for(File file:files){JSONObject item=new JSONObject(new String(Files.readAllBytes(file.toPath()),StandardCharsets.UTF_8));items.put(item.getString("id"),item);}
}
static boolean valid(String id){return id!=null && id.matches("user-[a-f0-9]{24}");}
private JSONObject record(JSONObject item) throws Exception {
String id=item.getString("id"),origin="http://"+id+".localhost:"+GaiaServer.PORT;
JSONObject manifest=new JSONObject(item.getJSONObject("manifest").toString());manifest.put("permissions",new JSONObject());manifest.put("type","web");
return new JSONObject().put("id",id).put("origin",origin).put("manifestURL",origin+"/manifest.webapp").put("manifest",manifest).put("removable",true).put("installed",true);
}
synchronized JSONArray list() throws Exception {JSONArray result=new JSONArray();for(JSONObject item:items.values())result.put(record(item));return result;}
synchronized void save(JSONObject item) throws Exception {
String id=item.getString("id");if(!valid(id) || !item.has("files"))throw new IOException("INVALID_PACKAGE");
byte[] data=item.toString().getBytes(StandardCharsets.UTF_8);if(data.length>70*1024*1024)throw new IOException("PACKAGE_TOO_LARGE");
File stage=new File(root,id+".tmp");try(FileOutputStream out=new FileOutputStream(stage)){out.write(data);out.getFD().sync();}
Files.move(stage.toPath(),new File(root,id+".json").toPath(),StandardCopyOption.REPLACE_EXISTING);items.put(id,item);
}
synchronized void remove(String id) throws Exception {if(!valid(id))throw new IOException("NOT_REMOVABLE");Files.deleteIfExists(new File(root,id+".json").toPath());items.remove(id);}
synchronized byte[] read(String id,String path) throws Exception {
JSONObject item=items.get(id);if(item==null)return null;
if(path.equals("manifest.webapp"))return record(item).getJSONObject("manifest").toString().getBytes(StandardCharsets.UTF_8);
String data=item.getJSONObject("files").optString(path,null);return data==null?null:android.util.Base64.decode(data,android.util.Base64.DEFAULT);
}
}

View file

@ -45,8 +45,7 @@ public final class MainActivity extends Activity {
root.setOnApplyWindowInsetsListener((v, insets) -> {
if (Build.VERSION.SDK_INT >= 30) {
android.graphics.Insets bars = insets.getInsets(
WindowInsets.Type.systemBars() |
WindowInsets.Type.ime() |
WindowInsets.Type.ime() |
WindowInsets.Type.displayCutout()
);
v.setPadding(bars.left, bars.top, bars.right, bars.bottom);
@ -54,11 +53,12 @@ public final class MainActivity extends Activity {
insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
insets.getSystemWindowInsetBottom()
insets.getSystemWindowInsetBottom() > dp(100) ? insets.getSystemWindowInsetBottom() : 0
);
return insets;
});
setContentView(root);
immersive();
// Respect Android's screen timeout; Gaia must not keep the phone awake indefinitely.
float brightness = app.getSharedPreferences("android", 0).getFloat("brightness", -1);
if (brightness >= 0) {
@ -166,6 +166,28 @@ public final class MainActivity extends Activity {
}
}
private void immersive() {
if (Build.VERSION.SDK_INT >= 30) {
getWindow().setDecorFitsSystemWindows(false);
WindowInsetsController controller = getWindow().getInsetsController();
if (controller != null) {
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
controller.hide(WindowInsets.Type.systemBars());
}
} else {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
}
@Override
public void onWindowFocusChanged(boolean focused) {
super.onWindowFocusChanged(focused);
if (focused) immersive();
}
private int dp(int value) {
return Math.round(value * getResources().getDisplayMetrics().density);
}
@ -274,7 +296,7 @@ public final class MainActivity extends Activity {
setHomeSurface(app.homeVisible);
}
private void closeBrowser() {
void closeBrowser() {
webPanel.setVisibility(View.GONE);
gaiaView.setVisibility(View.VISIBLE);
setHomeSurface(app.homeVisible);
@ -356,6 +378,7 @@ public final class MainActivity extends Activity {
public void onResume() {
super.onResume();
resumed = true;
immersive();
app.activity = this;
if (recoverGaia) {
recoverGaia = false;

View file

@ -17,9 +17,32 @@ public final class VulpesApplication extends Application {
GeckoSession gaiaSession;
AndroidServices services;
AndroidNotifications notifications;
boolean pendingScreenLock;
boolean homeVisible;
boolean homeAtBottom;
@Override
public void onCreate() {
super.onCreate();
android.content.BroadcastReceiver receiver = new android.content.BroadcastReceiver() {
@Override public void onReceive(android.content.Context context, android.content.Intent intent) {
pendingScreenLock = true;
sendScreenLock();
}
};
android.content.IntentFilter filter = new android.content.IntentFilter(android.content.Intent.ACTION_SCREEN_OFF);
if (android.os.Build.VERSION.SDK_INT >= 33) registerReceiver(receiver,filter,RECEIVER_NOT_EXPORTED);
else registerReceiver(receiver,filter);
}
void sendScreenLock() {
if (port == null || !pendingScreenLock) return;
try {
port.postMessage(new JSONObject().put("type","android-screen-off"));
pendingScreenLock = false;
} catch (Exception error) {Log.w("Vulpes","Screen lock pending",error);}
}
void prepare() throws IOException {
if (runtime != null) return;
server = new GaiaServer(this);
@ -58,6 +81,7 @@ public final class VulpesApplication extends Application {
@Override
public void onConnect(WebExtension.Port p) {
port = p;
sendScreenLock();
if (activity != null) notifications.click(activity.getIntent());
p.setDelegate(
new WebExtension.PortDelegate() {
@ -80,6 +104,18 @@ public final class VulpesApplication extends Application {
return GeckoResult.fromException(new SecurityException("EXTENSION_REQUIRED"));
}
JSONObject json = (JSONObject) message;
if (json.optString("type").equals("marketplace.return") && activity!=null) {activity.runOnUiThread(()->activity.closeBrowser());return GeckoResult.fromValue(null);}
if (json.optString("type").startsWith("packages.")) {
GeckoResult<Object> result=new GeckoResult<>();
new Thread(()->{try {
String operation=json.getString("type");
if(operation.equals("packages.save"))server.installed.save(json.getJSONObject("item"));
else if(operation.equals("packages.remove"))server.installed.remove(json.getString("id"));
else if(!operation.equals("packages.list"))throw new IOException("INVALID_OPERATION");
result.complete(server.installed.list().toString());
}catch(Exception error){result.completeExceptionally(error);}},"Vulpes-packages").start();
return result;
}
if (json.optString("type").equals("homeScroll")) {
homeAtBottom = json.optBoolean("bottom");
if (activity != null) activity.setHomeSurface(homeVisible);
@ -124,6 +160,7 @@ public final class VulpesApplication extends Application {
}
void resumed() {
sendScreenLock();
if (port == null) return;
try {
port.postMessage(new JSONObject().put("type", "android-resume"));