diff --git a/Android.bp b/Android.bp index 8b7118b..7706b21 100644 --- a/Android.bp +++ b/Android.bp @@ -1,3 +1,6 @@ +// PawletOS Framework API Extensions +// Installed to product partition; visible to system apps and vendor subpackages. + java_sdk_library { name: "pawlet-device", srcs: ["core/java/pawletos/device/PawletDevice.java"], @@ -15,7 +18,14 @@ java_sdk_library { java_sdk_library { name: "pawlet-system", - srcs: ["core/java/pawletos/device/system/PawletSystem.java"], + srcs: [ + "core/java/pawletos/device/system/PawletSystem.java", + "core/java/pawletos/device/system/PawletManager.java", + "core/java/pawletos/device/system/IPawletManager.aidl", + ], + aidl: { + local_include_dirs: ["core/java"], + }, api_packages: ["pawletos.device.system"], sdk_version: "system_current", product_specific: true, @@ -24,6 +34,20 @@ java_sdk_library { visibility: [ "//frameworks/base:__subpackages__", "//packages/apps:__subpackages__", + "//vendor:__subpackages__", ], unsafe_ignore_missing_latest_api: true, -} \ No newline at end of file +} + +// Service-side implementation — used by the privileged app that hosts the service. +java_library { + name: "pawlet-manager-service", + srcs: ["core/java/pawletos/device/system/PawletManagerService.java"], + libs: ["pawlet-system"], + sdk_version: "system_current", + product_specific: true, + visibility: [ + "//packages/apps:__subpackages__", + "//vendor:__subpackages__", + ], +} diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d36d542..7f72fc5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3,12 +3,12 @@ package="pawletos.device"> - + android:minSdkVersion="26" + android:targetSdkVersion="36" /> + - \ No newline at end of file + diff --git a/core/java/pawletos/device/PawletDevice.java b/core/java/pawletos/device/PawletDevice.java index 56c78c4..226a226 100644 --- a/core/java/pawletos/device/PawletDevice.java +++ b/core/java/pawletos/device/PawletDevice.java @@ -1,48 +1,79 @@ package pawletos.device; -import android.content.Context; +import android.os.Build; import android.os.SystemProperties; /** - * PawletOS Device API for apps running on PawletOS builds. + * Public PawletOS device information API. + * Available to all apps via the pawlet-device SDK library. */ -public class PawletDevice { +public final class PawletDevice { - /** - * Get the brand name. - */ - public static String getBrandName(Context ctx) { - int resId = ctx.getResources().getIdentifier("oxmc_brand_name", "string", ctx.getPackageName()); - if (resId != 0) { - return ctx.getString(resId); - } - return "PawletOS"; + private PawletDevice() {} + + /** Returns true if this device is running PawletOS. */ + public static boolean isPawletDevice() { + return !SystemProperties.get("ro.pawlet.build.version", "").isEmpty(); } - /** - * Get the codename. - */ - public static String getCodename(Context ctx) { - int resId = ctx.getResources().getIdentifier("oxmc_codename", "string", ctx.getPackageName()); - if (resId != 0) { - return ctx.getString(resId); - } - return "generic-pawlet-device"; + /** PawletOS major.minor version string, e.g. "1.0". */ + public static String getBuildVersion() { + return SystemProperties.get("ro.pawlet.build.version", "unknown"); } - // ───────────────────────────── - // Custom build property access - // ───────────────────────────── + /** Full display version including date and type, e.g. "1-20260101-RELEASE-pawlet_rpi4". */ + public static String getDisplayVersion() { + return SystemProperties.get("ro.pawlet.display.version", getBuildVersion()); + } + /** Release type: RELEASE, NIGHTLY, SNAPSHOT, EXPERIMENTAL, or UNOFFICIAL. */ + public static String getReleaseType() { + return SystemProperties.get("ro.pawlet.releasetype", "UNOFFICIAL"); + } + + /** Device codename as used in builds, e.g. "pawlet_rpi4". */ + public static String getDeviceCodename() { + return SystemProperties.get("ro.product.device", Build.DEVICE); + } + + /** Human-readable device model string. */ + public static String getDeviceModel() { + return SystemProperties.get("ro.product.model", Build.MODEL); + } + + /** Hardware platform, e.g. "bcm2711" (RPi 4) or "bcm2712" (RPi 5). */ + public static String getHardwarePlatform() { + return SystemProperties.get("ro.hardware", Build.HARDWARE); + } + + /** Device serial number. */ + public static String getSerialNumber() { + return SystemProperties.get("ro.serialno", Build.UNKNOWN); + } + + /** Build date as a Unix timestamp string from ro.build.date.utc. */ + public static long getBuildDate() { + try { + return Long.parseLong(SystemProperties.get("ro.build.date.utc", "0")); + } catch (NumberFormatException e) { + return 0L; + } + } + + // ── Legacy vendor properties ────────────────────────────────────────────── + + /** Manufacture date from vendor property, or "unknown". */ public static String getManufactureDate() { return SystemProperties.get("ro.oxmc.build.manufacture_date", "unknown"); } + /** Warranty exclusion string from vendor property, or "none". */ public static String getWarrantyExclusion() { return SystemProperties.get("ro.oxmc.build.warranty_exclusion", "none"); } + /** Product series from vendor property, or "generic". */ public static String getSeries() { return SystemProperties.get("ro.oxmc.build.series", "generic"); } -} \ No newline at end of file +} diff --git a/core/java/pawletos/device/system/IPawletManager.aidl b/core/java/pawletos/device/system/IPawletManager.aidl new file mode 100644 index 0000000..9c94cb5 --- /dev/null +++ b/core/java/pawletos/device/system/IPawletManager.aidl @@ -0,0 +1,61 @@ +package pawletos.device.system; + +/** + * System service interface for PawletOS platform APIs. + * Obtain via PawletManager.get(context). + * @hide + */ +interface IPawletManager { + + // ── Build info ──────────────────────────────────────────────────────────── + + /** PawletOS major.minor version string, e.g. "1.0". */ + String getPawletVersion(); + + /** Full display version, e.g. "1-20260101-RELEASE-pawlet_rpi4". */ + String getDisplayVersion(); + + /** Release type: RELEASE, NIGHTLY, SNAPSHOT, EXPERIMENTAL, or UNOFFICIAL. */ + String getReleaseType(); + + /** Build date as Unix timestamp. */ + long getBuildDate(); + + // ── Device info ─────────────────────────────────────────────────────────── + + /** Device codename, e.g. "pawlet_rpi4". */ + String getDeviceCodename(); + + /** Human-readable device model string. */ + String getDeviceModel(); + + /** Hardware platform, e.g. "bcm2711". */ + String getHardwarePlatform(); + + /** Device serial number. */ + String getSerialNumber(); + + // ── Provisioning ───────────────────────────────────────────────────────── + + /** True if DEVICE_PROVISIONED=1. */ + boolean isDeviceProvisioned(); + + /** True if USER_SETUP_COMPLETE=1. */ + boolean isUserSetupComplete(); + + // ── Feature flags ───────────────────────────────────────────────────────── + + /** + * Check if a named feature flag is enabled. + * Backed by persist.pawlet.feature.=1. + */ + boolean isFeatureEnabled(in String feature); + + /** Enable or disable a named feature flag (requires MANAGE_PAWLET_FEATURES permission). */ + void setFeatureEnabled(in String feature, boolean enabled); + + // ── SDK ─────────────────────────────────────────────────────────────────── + + /** PawletOS SDK/API version integer. */ + int getPawletSdkVersion(); +} diff --git a/core/java/pawletos/device/system/PawletManager.java b/core/java/pawletos/device/system/PawletManager.java new file mode 100644 index 0000000..50ba5eb --- /dev/null +++ b/core/java/pawletos/device/system/PawletManager.java @@ -0,0 +1,118 @@ +package pawletos.device.system; + +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +/** + * Client-side manager for the PawletOS system service. + * + * Usage: + * PawletManager pm = PawletManager.get(context); + * if (pm != null) { + * String version = pm.getPawletVersion(); + * } + * + * The service is registered by a privileged system app at boot via: + * ServiceManager.addService(PawletManager.SERVICE_NAME, new PawletManagerService(context)); + */ +public final class PawletManager { + + public static final String SERVICE_NAME = "pawlet"; + private static final String TAG = "PawletManager"; + + private final IPawletManager mService; + + private PawletManager(IPawletManager service) { + mService = service; + } + + /** + * Get the PawletManager instance, or null if the service is not running. + */ + public static PawletManager get(Context context) { + IBinder binder = ServiceManager.getService(SERVICE_NAME); + if (binder == null) { + Log.w(TAG, "pawlet service not available"); + return null; + } + return new PawletManager(IPawletManager.Stub.asInterface(binder)); + } + + // ── Build info ──────────────────────────────────────────────────────────── + + public String getPawletVersion() { + try { return mService.getPawletVersion(); } + catch (RemoteException e) { return null; } + } + + public String getDisplayVersion() { + try { return mService.getDisplayVersion(); } + catch (RemoteException e) { return null; } + } + + public String getReleaseType() { + try { return mService.getReleaseType(); } + catch (RemoteException e) { return null; } + } + + public long getBuildDate() { + try { return mService.getBuildDate(); } + catch (RemoteException e) { return 0L; } + } + + // ── Device info ─────────────────────────────────────────────────────────── + + public String getDeviceCodename() { + try { return mService.getDeviceCodename(); } + catch (RemoteException e) { return null; } + } + + public String getDeviceModel() { + try { return mService.getDeviceModel(); } + catch (RemoteException e) { return null; } + } + + public String getHardwarePlatform() { + try { return mService.getHardwarePlatform(); } + catch (RemoteException e) { return null; } + } + + public String getSerialNumber() { + try { return mService.getSerialNumber(); } + catch (RemoteException e) { return null; } + } + + // ── Provisioning ───────────────────────────────────────────────────────── + + public boolean isDeviceProvisioned() { + try { return mService.isDeviceProvisioned(); } + catch (RemoteException e) { return false; } + } + + public boolean isUserSetupComplete() { + try { return mService.isUserSetupComplete(); } + catch (RemoteException e) { return false; } + } + + // ── Feature flags ───────────────────────────────────────────────────────── + + public boolean isFeatureEnabled(String feature) { + try { return mService.isFeatureEnabled(feature); } + catch (RemoteException e) { return false; } + } + + public void setFeatureEnabled(String feature, boolean enabled) { + try { mService.setFeatureEnabled(feature, enabled); } + catch (RemoteException e) { Log.e(TAG, "setFeatureEnabled failed", e); } + } + + // ── SDK ─────────────────────────────────────────────────────────────────── + + public int getPawletSdkVersion() { + try { return mService.getPawletSdkVersion(); } + catch (RemoteException e) { return 0; } + } +} diff --git a/core/java/pawletos/device/system/PawletManagerService.java b/core/java/pawletos/device/system/PawletManagerService.java new file mode 100644 index 0000000..cc21edd --- /dev/null +++ b/core/java/pawletos/device/system/PawletManagerService.java @@ -0,0 +1,135 @@ +package pawletos.device.system; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.Binder; +import android.os.Build; +import android.os.SystemProperties; +import android.provider.Settings; +import android.util.Log; + +/** + * Implementation of IPawletManager, hosted by a privileged system app. + * + * The hosting app registers this on boot: + * ServiceManager.addService(PawletManager.SERVICE_NAME, new PawletManagerService(this)); + * + * Requires the hosting app to hold INTERACT_ACROSS_USERS_FULL for Settings access. + */ +public class PawletManagerService extends IPawletManager.Stub { + + public static final int PAWLET_SDK_VERSION = 1; + + private static final String TAG = "PawletManagerService"; + private static final String FEATURE_PROP_PREFIX = "persist.pawlet.feature."; + + private final Context mContext; + + public PawletManagerService(Context context) { + mContext = context.getApplicationContext(); + } + + // ── Build info ──────────────────────────────────────────────────────────── + + @Override + public String getPawletVersion() { + return SystemProperties.get("ro.pawlet.build.version", "unknown"); + } + + @Override + public String getDisplayVersion() { + return SystemProperties.get("ro.pawlet.display.version", getPawletVersion()); + } + + @Override + public String getReleaseType() { + return SystemProperties.get("ro.pawlet.releasetype", "UNOFFICIAL"); + } + + @Override + public long getBuildDate() { + try { + return Long.parseLong(SystemProperties.get("ro.build.date.utc", "0")); + } catch (NumberFormatException e) { + return 0L; + } + } + + // ── Device info ─────────────────────────────────────────────────────────── + + @Override + public String getDeviceCodename() { + return SystemProperties.get("ro.product.device", Build.DEVICE); + } + + @Override + public String getDeviceModel() { + return SystemProperties.get("ro.product.model", Build.MODEL); + } + + @Override + public String getHardwarePlatform() { + return SystemProperties.get("ro.hardware", Build.HARDWARE); + } + + @Override + public String getSerialNumber() { + return SystemProperties.get("ro.serialno", Build.UNKNOWN); + } + + // ── Provisioning ───────────────────────────────────────────────────────── + + @Override + public boolean isDeviceProvisioned() { + try { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) == 1; + } catch (Exception e) { + Log.w(TAG, "isDeviceProvisioned: " + e.getMessage()); + return false; + } + } + + @Override + public boolean isUserSetupComplete() { + try { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0) == 1; + } catch (Exception e) { + Log.w(TAG, "isUserSetupComplete: " + e.getMessage()); + return false; + } + } + + // ── Feature flags ───────────────────────────────────────────────────────── + + @Override + public boolean isFeatureEnabled(String feature) { + if (feature == null || feature.isEmpty()) return false; + return "1".equals(SystemProperties.get(FEATURE_PROP_PREFIX + feature, "0")); + } + + @Override + public void setFeatureEnabled(String feature, boolean enabled) { + if (feature == null || feature.isEmpty()) return; + enforceFeaturePermission(); + SystemProperties.set(FEATURE_PROP_PREFIX + feature, enabled ? "1" : "0"); + Log.i(TAG, "Feature " + feature + " " + (enabled ? "enabled" : "disabled") + + " by uid=" + Binder.getCallingUid()); + } + + // ── SDK ─────────────────────────────────────────────────────────────────── + + @Override + public int getPawletSdkVersion() { + return PAWLET_SDK_VERSION; + } + + // ── Internal ────────────────────────────────────────────────────────────── + + private void enforceFeaturePermission() { + mContext.enforceCallingOrSelfPermission( + "pawletos.permission.MANAGE_PAWLET_FEATURES", + "Requires MANAGE_PAWLET_FEATURES permission"); + } +} diff --git a/core/java/pawletos/device/system/PawletSystem.java b/core/java/pawletos/device/system/PawletSystem.java index dfe396d..0cc08d9 100644 --- a/core/java/pawletos/device/system/PawletSystem.java +++ b/core/java/pawletos/device/system/PawletSystem.java @@ -5,75 +5,72 @@ import android.os.Build; import android.os.SystemProperties; /** - * PawletOS System API for system apps (OTA, device management, etc.) + * Static system utilities for PawletOS system apps. + * For richer runtime data, use PawletManager.get(context) instead. */ -public class PawletSystem { - private static final double PAWLET_SDK_VERSION = 1.0; +public final class PawletSystem { - /** - * Check if running on an emulator. - */ + public static final int PAWLET_SDK_VERSION = 1; + + private PawletSystem() {} + + /** True if running on a PawletOS build. */ + public static boolean isPawletOS() { + return !SystemProperties.get("ro.pawlet.build.version", "").isEmpty(); + } + + /** True if this is a debug/userdebug build. */ + public static boolean isDebugBuild() { + return "1".equals(SystemProperties.get("ro.debuggable", "0")); + } + + /** True if this is a release-signed build (not test-keys). */ + public static boolean isReleaseBuild() { + String tags = Build.TAGS; + return tags != null && tags.contains("release-keys"); + } + + /** Current locale tag, e.g. "en_US". */ + public static String getCurrentLocale(Context ctx) { + return ctx.getResources().getConfiguration().getLocales().get(0).toString(); + } + + /** True if the device appears to be an emulator. */ public static boolean isEmulator() { - String fingerprint = Build.FINGERPRINT; - String model = Build.MODEL; - String manufacturer = Build.MANUFACTURER; - String brand = Build.BRAND; - String device = Build.DEVICE; - String product = Build.PRODUCT; - return fingerprint != null && (fingerprint.startsWith("generic") || fingerprint.startsWith("unknown")) - || model != null && model.contains("google_sdk") - || model != null && model.contains("Emulator") - || model != null && model.contains("Android SDK built for x86") - || manufacturer != null && manufacturer.contains("Genymotion") - || brand != null && brand.startsWith("generic") && device != null && device.startsWith("generic") - || product != null && product.equals("google_sdk"); + String fp = Build.FINGERPRINT; + return (fp != null && (fp.startsWith("generic") || fp.startsWith("unknown"))) + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || "google_sdk".equals(Build.PRODUCT); } /** - * Basic check if device is rooted. + * Heuristic check for whether the device has root access. + * Checks for su binaries in common locations and the ro.debuggable property. + * Not foolproof — a determined user can hide root. */ public static boolean isRooted() { - String buildTags = Build.TAGS; - if (buildTags != null && buildTags.contains("test-keys")) { - return true; - } - try { - java.io.File file = new java.io.File("/system/app/Superuser.apk"); - if (file.exists()) { - return true; - } - } catch (Exception e) { - // ignore + // Common su locations + String[] suPaths = { + "/system/bin/su", + "/system/xbin/su", + "/sbin/su", + "/vendor/bin/su", + "/system/su", + }; + for (String path : suPaths) { + if (new java.io.File(path).exists()) return true; } + // Magisk manager package presence + if (new java.io.File("/data/adb/magisk").exists()) return true; return false; } - /** - * Get the current language/locale. - */ - public static String getCurrentLocale(Context ctx) { - java.util.Locale locale; - if (android.os.Build.VERSION.SDK_INT >= 24) { - locale = ctx.getResources().getConfiguration().getLocales().get(0); - } else { - locale = ctx.getResources().getConfiguration().locale; - } - return locale.toString(); - } - - /** - * Check if device is running PawletOS. - * Returns true if running on PawletOS, false otherwise. - */ - public static boolean isPawletOS() { - return "Pawlet".equalsIgnoreCase(Build.MANUFACTURER) - || "PawletOS".equalsIgnoreCase(SystemProperties.get("ro.oxmc.os_name", "")); - } - - /** - * Get the PawletSDK version. - */ - public static double getPawletSDKVersion() { + /** PawletOS SDK version integer. Matches PawletManagerService.PAWLET_SDK_VERSION. */ + public static int getPawletSdkVersion() { return PAWLET_SDK_VERSION; } -} \ No newline at end of file +} diff --git a/sepolicy/file_contexts b/sepolicy/file_contexts index dd964ac..82d2c15 100644 --- a/sepolicy/file_contexts +++ b/sepolicy/file_contexts @@ -1,2 +1,3 @@ -# Label the JAR file -/system/framework/pawlet-device.jar u:object_r:pawlet_device_exec:s0 \ No newline at end of file +# PawletOS framework JARs +/system/framework/pawlet-device\.jar u:object_r:system_file:s0 +/system/framework/pawlet-system\.jar u:object_r:system_file:s0 diff --git a/sepolicy/pawlet_device.te b/sepolicy/pawlet_device.te index 99bfa68..1883583 100644 --- a/sepolicy/pawlet_device.te +++ b/sepolicy/pawlet_device.te @@ -1,14 +1,14 @@ -# Domain declaration +# PawletOS framework service domain type pawlet_device, domain, coredomain; type pawlet_device_exec, exec_type, file_type; -# Inherit from core domain -typeattribute pawlet_device coredomain; - -# Basic file access for your domain -allow pawlet_device pawlet_device_exec:file { execute read open map }; - -# Binder communication +# Binder communication with system_server and servicemanager binder_use(pawlet_device) binder_call(pawlet_device, system_server) -binder_call(pawlet_device, servicemanager) \ No newline at end of file +binder_call(pawlet_device, servicemanager) + +# Allow reading PawletOS system properties +get_prop(pawlet_device, system_prop) + +# Silence audit noise from system domains reading vendor props they don't need +dontaudit domain vendor_internal_prop:file { read open getattr map }; diff --git a/sepolicy/property_contexts b/sepolicy/property_contexts index a18f681..33cd8d2 100644 --- a/sepolicy/property_contexts +++ b/sepolicy/property_contexts @@ -1,4 +1,11 @@ -# Custom properties for the pawlet device -ro.vendor.oxmc.* u:object_r:vendor_default_prop:s0 -ro.vendor.pawlet.* u:object_r:vendor_default_prop:s0 -persist.vendor.pawlet.* u:object_r:vendor_default_prop:s0 \ No newline at end of file +# PawletOS system properties +ro.pawlet. u:object_r:system_prop:s0 +ro.oxmc. u:object_r:system_prop:s0 + +# Vendor-internal properties (vendor domain only) +ro.vendor.oxmc. u:object_r:vendor_internal_prop:s0 +ro.vendor.pawlet. u:object_r:vendor_internal_prop:s0 +persist.vendor.pawlet. u:object_r:vendor_internal_prop:s0 + +# Feature flags (writable by pawlet service) +persist.pawlet.feature. u:object_r:vendor_internal_prop:s0