diff --git a/Android.bp b/Android.bp index e1be444..d1fce44 100644 --- a/Android.bp +++ b/Android.bp @@ -2,7 +2,6 @@ android_app { name: "ConfigProvisioner", srcs: ["src/**/*.java"], manifest: "AndroidManifest.xml", - resource_dirs: ["res"], privileged: true, certificate: "platform", optimize: { @@ -12,4 +11,5 @@ android_app { enabled: false, }, product_specific: true, + sdk_version: "system_current", } \ No newline at end of file diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b1d45a5..6507630 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -9,6 +9,7 @@ + + diff --git a/README.md b/README.md index e7d5cbc..3233f00 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ -# app_ConfigProvisioner +# Config Provisioner + +## Overview +Config Provisioner is an Android system service and broadcast receiver designed to automate device provisioning based on vendor-specific configuration. On device boot, it checks for a vendor configuration file (`/vendor/etc/config_provisioner/vendor.cfg`). If provisioning is enabled and a valid APK URL is provided, it downloads and installs a configuration APK, then configures the device setup wizard according to vendor preferences. + +### Key Features +- Runs automatically on boot (including locked boot) +- Checks for vendor configuration file +- Downloads and installs a configuration APK if required +- Configures Android Setup Wizard (enables/disables, marks setup complete) +- Persists provisioning state to avoid repeated runs + +## Vendor Configuration File Format +The vendor configuration file is located at `/vendor/etc/config_provisioner/vendor.cfg`. It is a simple key-value file with one setting per line. Lines starting with `#` are comments and ignored. + +### Supported Keys +- `enable_setup_wizard` (boolean: `true`/`false`/`1`/`0`) + - Enables or disables the Android Setup Wizard after provisioning. +- `enable_provisioning` (boolean: `true`/`false`/`1`/`0`) + - Enables or disables the provisioning process. +- `vendor_id` (string) + - Identifier for the vendor/device. +- `network_timeout` (integer, milliseconds) + - Timeout for network operations (e.g., APK download). +- `config_apk_url` (string, URL) + - URL to the configuration APK to be downloaded and installed. + +### Example +``` +# Vendor configuration for Config Provisioner +enable_setup_wizard=false +enable_provisioning=true +vendor_id=acme_corp +network_timeout=30000 +config_apk_url=https://example.com/config/acme_config.apk +``` + +## Default Values +If a key is missing, the following defaults are used: +- `enable_setup_wizard`: true +- `enable_provisioning`: true +- `network_timeout`: 30000 +- `config_apk_url`: https://default.example.com/config.apk + +## Logging & Debugging +The service logs all configuration values and provisioning steps to logcat under the tags `ConfigProvisioner` and `VendorConfig`. + +## Permissions +The app requires system-level permissions to install packages, access network state, receive boot events, and modify setup wizard state. + +## License diff --git a/src/dev/oxmc/configprovisioner/BootReceiver.java b/src/dev/oxmc/configprovisioner/BootReceiver.java new file mode 100644 index 0000000..32e5489 --- /dev/null +++ b/src/dev/oxmc/configprovisioner/BootReceiver.java @@ -0,0 +1,67 @@ +package dev.oxmc.configprovisioner; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; + +public class BootReceiver extends BroadcastReceiver { + private static final String TAG = "ConfigProvisioner"; + private static final String PREF_NAME = "config_provisioner_prefs"; + static final String KEY_BASE_PROVISIONED = "has_base_provisioned"; + static final String KEY_LAST_OTA_CHECK = "last_ota_check_ms"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || intent.getAction() == null) return; + String action = intent.getAction(); + Log.d(TAG, "Received: " + action); + + switch (action) { + case Intent.ACTION_BOOT_COMPLETED: + case "android.intent.action.LOCKED_BOOT_COMPLETED": + if (!isBaseProvisioned(context)) { + startService(context, ProvisioningService.ACTION_BASE_PROVISION); + } + break; + + case Intent.ACTION_USER_PRESENT: + // Only run OTA check once base provisioning is complete and a URL is set. + if (isBaseProvisioned(context) && VendorConfig.isProvisioningEnabled() + && !VendorConfig.DEFAULT_CONFIG_APK_URL.equals( + VendorConfig.getConfigApkUrl())) { + startService(context, ProvisioningService.ACTION_OTA_UPDATE); + } + break; + } + } + + private static void startService(Context context, String action) { + Intent si = new Intent(context, ProvisioningService.class); + si.setAction(action); + context.startService(si); + } + + // ---- SharedPrefs helpers (package-private so ProvisioningService can use them) ---- + + static boolean isBaseProvisioned(Context context) { + return prefs(context).getBoolean(KEY_BASE_PROVISIONED, false); + } + + static void setBaseProvisioned(Context context, boolean value) { + prefs(context).edit().putBoolean(KEY_BASE_PROVISIONED, value).apply(); + } + + static long getLastOtaCheck(Context context) { + return prefs(context).getLong(KEY_LAST_OTA_CHECK, 0L); + } + + static void setLastOtaCheck(Context context, long timeMs) { + prefs(context).edit().putLong(KEY_LAST_OTA_CHECK, timeMs).apply(); + } + + private static SharedPreferences prefs(Context context) { + return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } +} diff --git a/src/dev/oxmc/configprovisioner/BootReciever.java b/src/dev/oxmc/configprovisioner/BootReciever.java deleted file mode 100644 index 48ac0eb..0000000 --- a/src/dev/oxmc/configprovisioner/BootReciever.java +++ /dev/null @@ -1,53 +0,0 @@ -package dev.oxmc.configprovisioner; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -public class BootReceiver extends BroadcastReceiver { - private static final String TAG = "ConfigProvisioner"; - private static final String PREF_NAME = "config_provisioner_prefs"; - private static final String KEY_HAS_PROVISIONED = "has_provisioned"; - - @Override - public void onReceive(Context context, Intent intent) { - if (intent == null || intent.getAction() == null) { - Log.d(TAG, "Null intent or action"); - return; - } - - Log.d(TAG, "Received boot event: " + intent.getAction()); - - // Check if we've already provisioned - if (hasProvisioned(context)) { - Log.d(TAG, "Already provisioned, skipping"); - return; - } - - // Check if vendor config exists - if not, exit gracefully - if (!VendorConfig.hasVendorConfig()) { - Log.i(TAG, "No vendor config found, provisioning not required"); - setProvisioned(context, true); // Mark as provisioned to not run again - return; - } - - // Start provisioning service - Intent serviceIntent = new Intent(context, ProvisioningService.class); - serviceIntent.setAction(intent.getAction()); - context.startService(serviceIntent); - } - - private boolean hasProvisioned(Context context) { - return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(KEY_HAS_PROVISIONED, false); - } - - public static void setProvisioned(Context context, boolean provisioned) { - context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(KEY_HAS_PROVISIONED, provisioned) - .apply(); - Log.d(TAG, "Provisioning state set to: " + provisioned); - } -} \ No newline at end of file diff --git a/src/dev/oxmc/configprovisioner/ProvisioningService.java b/src/dev/oxmc/configprovisioner/ProvisioningService.java index 88ec454..7f7c1b7 100644 --- a/src/dev/oxmc/configprovisioner/ProvisioningService.java +++ b/src/dev/oxmc/configprovisioner/ProvisioningService.java @@ -1,205 +1,468 @@ package dev.oxmc.configprovisioner; import android.app.Service; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.IBinder; import android.provider.Settings; +import android.provider.Telephony; import android.util.Log; +import android.util.Xml; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; +import org.xmlpull.v1.XmlPullParser; public class ProvisioningService extends Service { private static final String TAG = "ConfigProvisioner"; private static final String DOWNLOAD_PATH = "/data/local/tmp/config_provision.apk"; + /** Runs on BOOT_COMPLETED: applies built-in config APK settings + wizard state. No network. */ + public static final String ACTION_BASE_PROVISION = "dev.oxmc.configprovisioner.ACTION_BASE_PROVISION"; + + /** Runs on USER_PRESENT: downloads updated config APK from URL and re-applies settings. */ + public static final String ACTION_OTA_UPDATE = "dev.oxmc.configprovisioner.ACTION_OTA_UPDATE"; + + // ------------------------------------------------------------------------- + // Service entry point + // ------------------------------------------------------------------------- + @Override public int onStartCommand(Intent intent, int flags, int startId) { - Log.d(TAG, "Starting provisioning service"); - - // Double-check that vendor config exists - if (!VendorConfig.hasVendorConfig()) { - Log.i(TAG, "No vendor config found, stopping service"); - BootReceiver.setProvisioned(this, true); - stopSelf(); + String action = intent != null ? intent.getAction() : null; + if (ACTION_BASE_PROVISION.equals(action)) { + Log.d(TAG, "Starting base provisioning"); + new BaseProvisionTask().execute(); return START_NOT_STICKY; } - - new ProvisioningTask().execute(); - - return START_STICKY; + if (ACTION_OTA_UPDATE.equals(action)) { + Log.d(TAG, "Starting OTA config update check"); + new OtaUpdateTask().execute(); + return START_NOT_STICKY; + } + stopSelf(); + return START_NOT_STICKY; } - private class ProvisioningTask extends AsyncTask { + @Override + public IBinder onBind(Intent intent) { return null; } + + // ------------------------------------------------------------------------- + // Base provisioning — runs at boot, no network needed + // ------------------------------------------------------------------------- + + private class BaseProvisionTask extends AsyncTask { @Override - protected Boolean doInBackground(Void... voids) { - Log.i(TAG, "Starting provisioning process"); - - // Log all config values for debugging + protected Boolean doInBackground(Void... v) { + if (!VendorConfig.hasVendorConfig()) { + Log.i(TAG, "No vendor config, skipping base provisioning"); + return true; + } VendorConfig.logConfigValues(); - - // Check if provisioning is enabled - if (!VendorConfig.isProvisioningEnabled()) { - Log.i(TAG, "Provisioning disabled by vendor config"); - configureSetupWizard(); - return true; - } - - // Get config URL - String configUrl = VendorConfig.getConfigApkUrl(); - if (configUrl == null || configUrl.isEmpty() || configUrl.equals(VendorConfig.DEFAULT_CONFIG_APK_URL)) { - Log.e(TAG, "No valid config URL configured, skipping provisioning"); - configureSetupWizard(); - return true; - } - - try { - Log.d(TAG, "Downloading config from: " + configUrl); - - if (downloadApk(configUrl, DOWNLOAD_PATH)) { - Log.i(TAG, "Download successful, installing APK"); - - if (installApk(DOWNLOAD_PATH)) { - Log.i(TAG, "Installation successful"); - configureSetupWizard(); - return true; - } else { - Log.e(TAG, "Installation failed"); - } - } else { - Log.e(TAG, "Download failed"); - } - } catch (Exception e) { - Log.e(TAG, "Provisioning failed with error", e); - } finally { - // Clean up downloaded file - File downloadedFile = new File(DOWNLOAD_PATH); - if (downloadedFile.exists()) { - if (downloadedFile.delete()) { - Log.d(TAG, "Cleaned up downloaded file"); - } else { - Log.w(TAG, "Failed to clean up downloaded file"); - } - } - } - - return false; + // Apply settings and APNs from the pre-installed config APK (built into the ROM). + // No download needed — the APK is already present as a system app. + applyConfigApkSettings(); + applyApns(); + configureSetupWizard(); + return true; } @Override protected void onPostExecute(Boolean success) { - if (success) { - Log.i(TAG, "Provisioning completed successfully"); - BootReceiver.setProvisioned(ProvisioningService.this, true); - } else { - Log.e(TAG, "Provisioning failed"); - // Don't mark as provisioned on failure, so it retries on next boot - } + BootReceiver.setBaseProvisioned(ProvisioningService.this, true); + Log.i(TAG, "Base provisioning complete"); stopSelf(); } - - private boolean downloadApk(String urlString, String outputPath) { - HttpURLConnection connection = null; + } + + // ------------------------------------------------------------------------- + // OTA update — runs on USER_PRESENT, deferred well after boot + // ------------------------------------------------------------------------- + + private class OtaUpdateTask extends AsyncTask { + @Override + protected Boolean doInBackground(Void... v) { + if (!VendorConfig.hasVendorConfig() || !VendorConfig.isProvisioningEnabled()) { + return false; + } + String url = VendorConfig.getConfigApkUrl(); + if (url == null || url.isEmpty() || url.equals(VendorConfig.DEFAULT_CONFIG_APK_URL)) { + Log.d(TAG, "No OTA URL configured, skipping"); + return false; + } + + long now = System.currentTimeMillis(); + long lastCheck = BootReceiver.getLastOtaCheck(ProvisioningService.this); + long interval = VendorConfig.getOtaCheckIntervalMs(); + if (now - lastCheck < interval) { + long minutesLeft = (interval - (now - lastCheck)) / 60_000; + Log.d(TAG, "OTA check skipped — next check in " + minutesLeft + " min"); + return false; + } + + // Record attempt time before the download so a broken server doesn't + // cause a retry on every subsequent screen unlock. + BootReceiver.setLastOtaCheck(ProvisioningService.this, now); + try { - URL url = new URL(urlString); - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(VendorConfig.getNetworkTimeout()); - connection.setReadTimeout(VendorConfig.getNetworkTimeout()); - connection.setRequestProperty("User-Agent", "ConfigProvisioner/1.0"); - connection.connect(); - - int responseCode = connection.getResponseCode(); - if (responseCode != HttpURLConnection.HTTP_OK) { - Log.e(TAG, "Server returned HTTP " + responseCode); - return false; - } - - try (InputStream input = connection.getInputStream(); - FileOutputStream output = new FileOutputStream(outputPath)) { - - byte[] buffer = new byte[8192]; - int bytesRead; - long totalBytes = 0; - - while ((bytesRead = input.read(buffer)) != -1) { - output.write(buffer, 0, bytesRead); - totalBytes += bytesRead; - } - - Log.d(TAG, "Downloaded " + totalBytes + " bytes to " + outputPath); - return true; - } + Log.i(TAG, "Downloading config update from: " + url); + if (!downloadApk(url, DOWNLOAD_PATH)) return false; + if (!installApk(DOWNLOAD_PATH)) return false; + Log.i(TAG, "OTA config APK installed, re-applying settings"); + applyConfigApkSettings(); + applyApns(); + return true; } catch (Exception e) { - Log.e(TAG, "Download failed", e); + Log.e(TAG, "OTA update failed", e); return false; } finally { - if (connection != null) { - connection.disconnect(); - } + new File(DOWNLOAD_PATH).delete(); } } - - private boolean installApk(String apkPath) { - try { - Process process = Runtime.getRuntime().exec( - new String[]{"pm", "install", "-r", "--user", "0", apkPath} - ); - - int exitCode = process.waitFor(); - if (exitCode == 0) { - Log.i(TAG, "APK installed successfully via pm install"); - return true; - } else { - Log.e(TAG, "pm install failed with exit code: " + exitCode); - return false; - } - } catch (Exception e) { - Log.e(TAG, "Installation failed", e); + + @Override + protected void onPostExecute(Boolean updated) { + if (updated) Log.i(TAG, "OTA config update applied successfully"); + stopSelf(); + } + } + + // ------------------------------------------------------------------------- + // Network helpers + // ------------------------------------------------------------------------- + + private boolean downloadApk(String urlString, String outputPath) { + HttpURLConnection connection = null; + try { + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(VendorConfig.getNetworkTimeout()); + connection.setReadTimeout(VendorConfig.getNetworkTimeout()); + connection.setRequestProperty("User-Agent", "ConfigProvisioner/1.0"); + connection.connect(); + + int code = connection.getResponseCode(); + if (code != HttpURLConnection.HTTP_OK) { + Log.e(TAG, "Server returned HTTP " + code); return false; } + + try (InputStream in = connection.getInputStream(); + FileOutputStream out = new FileOutputStream(outputPath)) { + byte[] buf = new byte[8192]; + int n; + long total = 0; + while ((n = in.read(buf)) != -1) { out.write(buf, 0, n); total += n; } + Log.d(TAG, "Downloaded " + total + " bytes"); + return total > 0; + } + } catch (Exception e) { + Log.e(TAG, "Download failed", e); + return false; + } finally { + if (connection != null) connection.disconnect(); } - - private void configureSetupWizard() { - boolean enableWizard = VendorConfig.isSetupWizardEnabled(); - Log.i(TAG, "Configuring Setup Wizard: " + (enableWizard ? "ENABLED" : "DISABLED")); - - if (!enableWizard) { - disablePackage("com.android.setupwizard"); - disablePackage("com.google.android.setupwizard"); - disablePackage("org.lineageos.setupwizard"); - - // Mark setup as complete - try { - Settings.Secure.putInt(getContentResolver(), - Settings.Secure.USER_SETUP_COMPLETE, 1); - Settings.Global.putInt(getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 1); - Log.d(TAG, "Setup marked as complete"); - } catch (Exception e) { - Log.w(TAG, "Failed to set setup complete flags", e); + } + + private boolean installApk(String apkPath) { + try { + Process p = Runtime.getRuntime().exec( + new String[]{"pm", "install", "-r", "--user", "0", apkPath}); + int exit = p.waitFor(); + if (exit == 0) { Log.i(TAG, "APK installed via pm install"); return true; } + Log.e(TAG, "pm install failed (exit " + exit + ")"); + return false; + } catch (Exception e) { + Log.e(TAG, "Installation failed", e); + return false; + } + } + + // ------------------------------------------------------------------------- + // Settings application + // ------------------------------------------------------------------------- + + private void applyConfigApkSettings() { + String pkg = VendorConfig.getConfigApkPackage(); + if (pkg == null || pkg.isEmpty()) return; + try { + Context ctx = createPackageContext(pkg, Context.CONTEXT_IGNORE_SECURITY); + Resources res = ctx.getResources(); + int xmlId = res.getIdentifier("settings", "xml", pkg); + if (xmlId == 0) { Log.d(TAG, "No settings.xml in " + pkg); return; } + + XmlResourceParser xml = res.getXml(xmlId); + int event, skipDepth = 0; + while ((event = xml.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + if (skipDepth > 0) { skipDepth++; continue; } + String tag = xml.getName(); + if ("if".equals(tag)) { if (!evaluateCondition(xml)) skipDepth = 1; continue; } + String name = xml.getAttributeValue(null, "name"); + String value = xml.getAttributeValue(null, "value"); + if (name == null || value == null) continue; + try { + switch (tag) { + case "secure": Settings.Secure.putString(getContentResolver(), name, value); break; + case "system": Settings.System.putString(getContentResolver(), name, value); break; + case "global": Settings.Global.putString(getContentResolver(), name, value); break; + default: Log.w(TAG, "Unknown settings tag: " + tag); + } + Log.d(TAG, tag + "." + name + " = " + value); + } catch (Exception e) { + Log.w(TAG, "Failed to apply " + tag + "." + name, e); + } + } else if (event == XmlResourceParser.END_TAG) { + if (skipDepth > 0) skipDepth--; } } + xml.close(); + Log.i(TAG, "Settings from " + pkg + " applied"); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Config APK not found: " + pkg + " (not yet installed?)"); + } catch (Exception e) { + Log.w(TAG, "Failed to apply config APK settings", e); } - - private void disablePackage(String packageName) { + } + + private void applyApns() { + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) return; + String pkg = VendorConfig.getConfigApkPackage(); + if (pkg == null || pkg.isEmpty()) return; + try { + Context ctx = createPackageContext(pkg, Context.CONTEXT_IGNORE_SECURITY); + int[] counts = {0, 0}; + + AssetManager assets = ctx.getAssets(); + String[] topLevel = assets.list("apns"); + if (topLevel != null && topLevel.length > 0) { + walkApnAssets(assets, "apns", counts); + Log.i(TAG, "APNs (assets/apns/): " + counts[0] + " inserted, " + counts[1] + " skipped"); + return; + } + + Resources res = ctx.getResources(); + int xmlId = res.getIdentifier("apns", "xml", pkg); + if (xmlId == 0) { Log.d(TAG, "No APN config in " + pkg); return; } + XmlResourceParser xml = res.getXml(xmlId); + parseAndInsertApns(xml, "res/xml/apns.xml", counts); + xml.close(); + Log.i(TAG, "APNs (res/xml/apns.xml): " + counts[0] + " inserted, " + counts[1] + " skipped"); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Config APK not found for APN provisioning: " + pkg); + } catch (Exception e) { + Log.w(TAG, "APN provisioning failed", e); + } + } + + private void walkApnAssets(AssetManager assets, String path, int[] counts) { + try { + String[] entries = assets.list(path); + if (entries == null) return; + for (String entry : entries) { + String full = path + "/" + entry; + String[] children = assets.list(full); + if (children != null && children.length > 0) { + walkApnAssets(assets, full, counts); + } else if (entry.endsWith(".xml")) { + try (InputStream is = assets.open(full)) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(is, "UTF-8"); + parseAndInsertApns(parser, full, counts); + } catch (Exception e) { + Log.w(TAG, "Failed to parse APN file: " + full, e); + } + } + } + } catch (Exception e) { + Log.w(TAG, "Failed to enumerate assets/" + path, e); + } + } + + private void parseAndInsertApns(XmlPullParser parser, String source, int[] counts) + throws Exception { + int event; + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (event != XmlPullParser.START_TAG || !"apn".equals(parser.getName())) continue; + + String name = parser.getAttributeValue(null, "name"); + if (name == null) name = parser.getAttributeValue(null, "carrier"); + String mcc = parser.getAttributeValue(null, "mcc"); + String mnc = parser.getAttributeValue(null, "mnc"); + String apn = parser.getAttributeValue(null, "apn"); + String numeric = parser.getAttributeValue(null, "numeric"); + String mvnoType = parser.getAttributeValue(null, "mvno_type"); + String mvnoData = parser.getAttributeValue(null, "mvno_match_data"); + if (numeric == null && mcc != null && mnc != null) numeric = mcc + mnc; + if (mvnoType == null) mvnoType = ""; + if (mvnoData == null) mvnoData = ""; + + if (name == null || numeric == null || mcc == null || mnc == null || apn == null) { + Log.w(TAG, "Skipping APN with missing fields in " + source); + continue; + } + + Cursor c = getContentResolver().query(Telephony.Carriers.CONTENT_URI, + new String[]{"_id"}, + "numeric=? AND apn=? AND mvno_type=? AND mvno_match_data=?", + new String[]{numeric, apn, mvnoType, mvnoData}, null); + boolean exists = c != null && c.getCount() > 0; + if (c != null) c.close(); + if (exists) { counts[1]++; continue; } + + ContentValues cv = new ContentValues(); + cv.put(Telephony.Carriers.NAME, name); + cv.put(Telephony.Carriers.NUMERIC, numeric); + cv.put(Telephony.Carriers.MCC, mcc); + cv.put(Telephony.Carriers.MNC, mnc); + cv.put(Telephony.Carriers.APN, apn); + if (!mvnoType.isEmpty()) cv.put(Telephony.Carriers.MVNO_TYPE, mvnoType); + if (!mvnoData.isEmpty()) cv.put(Telephony.Carriers.MVNO_MATCH_DATA, mvnoData); + + String type = parser.getAttributeValue(null, "type"); + cv.put(Telephony.Carriers.TYPE, type != null ? type : "default,supl"); + String proto = parser.getAttributeValue(null, "protocol"); + cv.put(Telephony.Carriers.PROTOCOL, proto != null ? proto : "IPV4V6"); + String roamProto = parser.getAttributeValue(null, "roaming_protocol"); + cv.put(Telephony.Carriers.ROAMING_PROTOCOL, roamProto != null ? roamProto : "IPV4V6"); + + apnPutStr(cv, Telephony.Carriers.SERVER, parser, "server"); + apnPutStr(cv, Telephony.Carriers.PROXY, parser, "proxy"); + apnPutStr(cv, Telephony.Carriers.PORT, parser, "port"); + apnPutStr(cv, Telephony.Carriers.MMSC, parser, "mmsc"); + apnPutStr(cv, Telephony.Carriers.MMSPROXY, parser, "mmsproxy"); + apnPutStr(cv, Telephony.Carriers.MMSPORT, parser, "mmsport"); + apnPutStr(cv, Telephony.Carriers.USER, parser, "user"); + apnPutStr(cv, Telephony.Carriers.PASSWORD, parser, "password"); + apnPutStr(cv, "bearer_bitmask", parser, "bearer_bitmask"); + apnPutInt(cv, Telephony.Carriers.AUTH_TYPE, parser, "authtype", -1); + apnPutInt(cv, "profile_id", parser, "profile_id", 0); + apnPutInt(cv, "max_conns", parser, "max_conns", 0); + apnPutInt(cv, "wait_time", parser, "wait_time", 0); + apnPutInt(cv, "max_conns_time", parser, "max_conns_time", 0); + apnPutInt(cv, Telephony.Carriers.MTU, parser, "mtu", 0); + + String modemCog = parser.getAttributeValue(null, "modem_cognitive"); + if (modemCog != null) + cv.put("modem_cognitive", + "true".equalsIgnoreCase(modemCog) || "1".equals(modemCog) ? 1 : 0); + String enabled = parser.getAttributeValue(null, "carrier_enabled"); + cv.put(Telephony.Carriers.CARRIER_ENABLED, + "0".equals(enabled) || "false".equalsIgnoreCase(enabled) ? 0 : 1); + try { - PackageManager pm = getPackageManager(); - pm.setApplicationEnabledSetting(packageName, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0); - Log.d(TAG, "Disabled package: " + packageName); + getContentResolver().insert(Telephony.Carriers.CONTENT_URI, cv); + counts[0]++; } catch (Exception e) { - Log.w(TAG, "Failed to disable package: " + packageName, e); + Log.w(TAG, "Failed to insert APN: " + name, e); } } } - @Override - public IBinder onBind(Intent intent) { - return null; + private static void apnPutStr(ContentValues cv, String col, XmlPullParser p, String attr) { + String v = p.getAttributeValue(null, attr); + if (v != null && !v.isEmpty()) cv.put(col, v); } -} \ No newline at end of file + + private static void apnPutInt(ContentValues cv, String col, XmlPullParser p, + String attr, int def) { + String v = p.getAttributeValue(null, attr); + if (v == null || v.isEmpty()) return; + try { cv.put(col, Integer.parseInt(v)); } + catch (NumberFormatException e) { if (def >= 0) cv.put(col, def); } + } + + // ------------------------------------------------------------------------- + // Setup wizard configuration + // ------------------------------------------------------------------------- + + private void configureSetupWizard() { + boolean enable = VendorConfig.isSetupWizardEnabled(); + Log.i(TAG, "Setup wizard: " + (enable ? "ENABLED" : "DISABLED")); + if (enable) { + try { + Settings.Secure.putInt(getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE, 0); + Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0); + } catch (Exception e) { + Log.w(TAG, "Failed to mark setup incomplete", e); + } + } else { + disablePackage("com.android.setupwizard"); + disablePackage("com.google.android.setupwizard"); + disablePackage("org.lineageos.setupwizard"); + try { + Settings.Secure.putInt(getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE, 1); + Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1); + } catch (Exception e) { + Log.w(TAG, "Failed to mark setup complete", e); + } + } + } + + private void disablePackage(String pkg) { + try { + getPackageManager().setApplicationEnabledSetting( + pkg, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0); + Log.d(TAG, "Disabled: " + pkg); + } catch (Exception e) { + Log.w(TAG, "Could not disable " + pkg + ": " + e.getMessage()); + } + } + + // ------------------------------------------------------------------------- + // condition evaluation + // ------------------------------------------------------------------------- + + private boolean evaluateCondition(XmlResourceParser xml) { + String manufacturer = xml.getAttributeValue(null, "manufacturer"); + String brand = xml.getAttributeValue(null, "brand"); + String model = xml.getAttributeValue(null, "model"); + String sdkStr = xml.getAttributeValue(null, "sdk"); + String sdkMinStr = xml.getAttributeValue(null, "sdk_min"); + String sdkMaxStr = xml.getAttributeValue(null, "sdk_max"); + String formFactor = xml.getAttributeValue(null, "form_factor"); + String feature = xml.getAttributeValue(null, "feature"); + + if (manufacturer != null && !Build.MANUFACTURER.equalsIgnoreCase(manufacturer)) return false; + if (brand != null && !Build.BRAND.equalsIgnoreCase(brand)) return false; + if (model != null && !Build.MODEL.toLowerCase().contains(model.toLowerCase())) return false; + + if (sdkStr != null) { try { if (Build.VERSION.SDK_INT != Integer.parseInt(sdkStr)) return false; } catch (NumberFormatException ignored) {} } + if (sdkMinStr != null) { try { if (Build.VERSION.SDK_INT < Integer.parseInt(sdkMinStr)) return false; } catch (NumberFormatException ignored) {} } + if (sdkMaxStr != null) { try { if (Build.VERSION.SDK_INT > Integer.parseInt(sdkMaxStr)) return false; } catch (NumberFormatException ignored) {} } + + if (formFactor != null) { + switch (formFactor) { + case "tablet": if (!isTablet()) return false; break; + case "phone": if (isTablet()) return false; break; + case "flip": + if (!getPackageManager().hasSystemFeature("android.hardware.sensor.hinge_angle")) return false; + break; + case "tv": + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) return false; + break; + default: Log.w(TAG, "Unknown form_factor: " + formFactor); + } + } + if (feature != null && !getPackageManager().hasSystemFeature(feature)) return false; + return true; + } + + private boolean isTablet() { + int layout = getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK; + return layout >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } +} diff --git a/src/dev/oxmc/configprovisioner/VendorConfig.java b/src/dev/oxmc/configprovisioner/VendorConfig.java index 7ed9d2a..d7228fe 100644 --- a/src/dev/oxmc/configprovisioner/VendorConfig.java +++ b/src/dev/oxmc/configprovisioner/VendorConfig.java @@ -8,118 +8,106 @@ import java.io.IOException; public class VendorConfig { private static final String TAG = "VendorConfig"; - - // Configuration path + public static final String VENDOR_CONFIG_PATH = "/vendor/etc/config_provisioner/vendor.cfg"; - - // Configuration keys - private static final String KEY_ENABLE_SETUP_WIZARD = "enable_setup_wizard"; - private static final String KEY_ENABLE_PROVISIONING = "enable_provisioning"; - private static final String KEY_VENDOR_ID = "vendor_id"; - private static final String KEY_NETWORK_TIMEOUT = "network_timeout"; - private static final String KEY_CONFIG_APK_URL = "config_apk_url"; - - // Default values - public static final boolean DEFAULT_ENABLE_SETUP_WIZARD = true; - public static final boolean DEFAULT_ENABLE_PROVISIONING = true; - public static final int DEFAULT_NETWORK_TIMEOUT = 30000; - public static final String DEFAULT_CONFIG_APK_URL = "https://default.example.com/config.apk"; - + + private static final String KEY_ENABLE_SETUP_WIZARD = "enable_setup_wizard"; + private static final String KEY_ENABLE_PROVISIONING = "enable_provisioning"; + private static final String KEY_VENDOR_ID = "vendor_id"; + private static final String KEY_NETWORK_TIMEOUT = "network_timeout"; + private static final String KEY_CONFIG_APK_URL = "config_apk_url"; + private static final String KEY_CONFIG_APK_PACKAGE = "config_apk_package"; + private static final String KEY_OTA_CHECK_INTERVAL = "ota_check_interval_ms"; + + public static final boolean DEFAULT_ENABLE_SETUP_WIZARD = false; + public static final boolean DEFAULT_ENABLE_PROVISIONING = false; + public static final int DEFAULT_NETWORK_TIMEOUT = 30_000; + public static final long DEFAULT_OTA_INTERVAL_MS = 24L * 60 * 60 * 1000; // 24 h + public static final String DEFAULT_CONFIG_APK_URL = "https://default.example.com/config.apk"; + public static boolean hasVendorConfig() { - File configFile = new File(VENDOR_CONFIG_PATH); - boolean exists = configFile.exists(); + boolean exists = new File(VENDOR_CONFIG_PATH).exists(); Log.d(TAG, "Vendor config exists: " + exists + " at " + VENDOR_CONFIG_PATH); return exists; } - + public static boolean isSetupWizardEnabled() { - return getConfigBoolean(KEY_ENABLE_SETUP_WIZARD, DEFAULT_ENABLE_SETUP_WIZARD); + return getBoolean(KEY_ENABLE_SETUP_WIZARD, DEFAULT_ENABLE_SETUP_WIZARD); } - + public static boolean isProvisioningEnabled() { - return getConfigBoolean(KEY_ENABLE_PROVISIONING, DEFAULT_ENABLE_PROVISIONING); + return getBoolean(KEY_ENABLE_PROVISIONING, DEFAULT_ENABLE_PROVISIONING); } - + public static String getConfigApkUrl() { - return getConfigString(KEY_CONFIG_APK_URL, DEFAULT_CONFIG_APK_URL); + return getString(KEY_CONFIG_APK_URL, DEFAULT_CONFIG_APK_URL); } - + + public static String getConfigApkPackage() { + return getString(KEY_CONFIG_APK_PACKAGE, "app.pawlet.config"); + } + public static String getVendorId() { - return getConfigString(KEY_VENDOR_ID, "default_vendor"); + return getString(KEY_VENDOR_ID, "default_vendor"); } - + public static int getNetworkTimeout() { try { - return Integer.parseInt(getConfigString(KEY_NETWORK_TIMEOUT, - String.valueOf(DEFAULT_NETWORK_TIMEOUT))); + return Integer.parseInt(getString(KEY_NETWORK_TIMEOUT, + String.valueOf(DEFAULT_NETWORK_TIMEOUT))); } catch (NumberFormatException e) { return DEFAULT_NETWORK_TIMEOUT; } } - - private static boolean getConfigBoolean(String key, boolean defaultValue) { - String value = getConfigValue(key); - if (value != null) { - return "true".equalsIgnoreCase(value) || "1".equals(value); + + public static long getOtaCheckIntervalMs() { + try { + return Long.parseLong(getString(KEY_OTA_CHECK_INTERVAL, + String.valueOf(DEFAULT_OTA_INTERVAL_MS))); + } catch (NumberFormatException e) { + return DEFAULT_OTA_INTERVAL_MS; } - - // Fallback to system properties if config file doesn't exist - if (!hasVendorConfig()) { - String propValue = android.os.SystemProperties.get("persist.configprovisioner." + key, ""); - if (!propValue.isEmpty()) { - return "true".equalsIgnoreCase(propValue) || "1".equals(propValue); - } - } - - return defaultValue; } - - private static String getConfigString(String key, String defaultValue) { - String value = getConfigValue(key); - if (value != null) return value; - - // Fallback to system properties if config file doesn't exist - if (!hasVendorConfig()) { - return android.os.SystemProperties.get("persist.configprovisioner." + key, defaultValue); - } - - return defaultValue; + + public static void logConfigValues() { + if (!hasVendorConfig()) { Log.d(TAG, "No vendor config file found"); return; } + Log.d(TAG, KEY_ENABLE_SETUP_WIZARD + "=" + isSetupWizardEnabled()); + Log.d(TAG, KEY_ENABLE_PROVISIONING + "=" + isProvisioningEnabled()); + Log.d(TAG, KEY_CONFIG_APK_URL + "=" + getConfigApkUrl()); + Log.d(TAG, KEY_VENDOR_ID + "=" + getVendorId()); + Log.d(TAG, KEY_NETWORK_TIMEOUT + "=" + getNetworkTimeout()); + Log.d(TAG, KEY_OTA_CHECK_INTERVAL + "=" + getOtaCheckIntervalMs()); } - - private static String getConfigValue(String key) { - if (!hasVendorConfig()) { - return null; - } - - try (BufferedReader reader = new BufferedReader(new FileReader(VENDOR_CONFIG_PATH))) { + + private static boolean getBoolean(String key, boolean def) { + String v = rawValue(key); + if (v != null) return "true".equalsIgnoreCase(v) || "1".equals(v); + String prop = android.os.SystemProperties.get("persist.configprovisioner." + key, ""); + if (!prop.isEmpty()) return "true".equalsIgnoreCase(prop) || "1".equals(prop); + return def; + } + + private static String getString(String key, String def) { + String v = rawValue(key); + if (v != null) return v; + String prop = android.os.SystemProperties.get("persist.configprovisioner." + key, ""); + if (!prop.isEmpty()) return prop; + return def; + } + + private static String rawValue(String key) { + if (!new File(VENDOR_CONFIG_PATH).exists()) return null; + try (BufferedReader r = new BufferedReader(new FileReader(VENDOR_CONFIG_PATH))) { String line; - while ((line = reader.readLine()) != null) { + while ((line = r.readLine()) != null) { line = line.trim(); if (line.startsWith("#") || line.isEmpty()) continue; - String[] parts = line.split("=", 2); - if (parts.length == 2 && parts[0].trim().equals(key)) { - return parts[1].trim(); - } + if (parts.length == 2 && parts[0].trim().equals(key)) return parts[1].trim(); } } catch (IOException e) { Log.e(TAG, "Error reading vendor config", e); } return null; } - - // Helper method to get all config values for debugging - public static void logConfigValues() { - if (!hasVendorConfig()) { - Log.d(TAG, "No vendor config file found"); - return; - } - - Log.d(TAG, "Current configuration:"); - Log.d(TAG, KEY_ENABLE_SETUP_WIZARD + "=" + isSetupWizardEnabled()); - Log.d(TAG, KEY_ENABLE_PROVISIONING + "=" + isProvisioningEnabled()); - Log.d(TAG, KEY_CONFIG_APK_URL + "=" + getConfigApkUrl()); - Log.d(TAG, KEY_VENDOR_ID + "=" + getVendorId()); - Log.d(TAG, KEY_NETWORK_TIMEOUT + "=" + getNetworkTimeout()); - } -} \ No newline at end of file +}