frameworks_pawlet: add PawletManager service + clean up APIs

Add IPawletManager AIDL interface, PawletManager client wrapper, and
PawletManagerService implementation. The service is hosted by a
privileged system app which calls:
  ServiceManager.addService(PawletManager.SERVICE_NAME, new PawletManagerService(this));

API surface:
  - Build info: getPawletVersion, getDisplayVersion, getReleaseType, getBuildDate
  - Device info: getDeviceCodename, getDeviceModel, getHardwarePlatform, getSerialNumber
  - Provisioning: isDeviceProvisioned, isUserSetupComplete
  - Feature flags: isFeatureEnabled / setFeatureEnabled (persist.pawlet.feature.*)
  - SDK version: getPawletSdkVersion

PawletDevice: remove broken resource lookup, use system props directly;
add isPawletDevice, getBuildVersion, getDisplayVersion, getReleaseType,
getHardwarePlatform, getSerialNumber, getBuildDate.

PawletSystem: remove dead SDK<24 branches; improve isRooted to check su
binary paths and Magisk; keep isEmulator with same logic.

sepolicy: vendor_internal_prop for persist.* and vendor.* props,
dontaudit for system domains; fix file_contexts JAR labels.

Android.bp: add aidl block, pawlet-manager-service java_library.
AndroidManifest: minSdk 26, targetSdk 36.
This commit is contained in:
oxmc
2026-06-12 20:02:46 -07:00
parent 51a93970d9
commit ec3e75de5d
10 changed files with 478 additions and 104 deletions

View File

@@ -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");
}
}
}

View File

@@ -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.<name>=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();
}

View File

@@ -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; }
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}
}