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
+}