diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..e1be444
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,15 @@
+android_app {
+ name: "ConfigProvisioner",
+ srcs: ["src/**/*.java"],
+ manifest: "AndroidManifest.xml",
+ resource_dirs: ["res"],
+ privileged: true,
+ certificate: "platform",
+ optimize: {
+ enabled: false,
+ },
+ dex_preopt: {
+ enabled: false,
+ },
+ product_specific: true,
+}
\ No newline at end of file
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..b1d45a5
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/dev/oxmc/configprovisioner/BootReciever.java b/src/dev/oxmc/configprovisioner/BootReciever.java
new file mode 100644
index 0000000..48ac0eb
--- /dev/null
+++ b/src/dev/oxmc/configprovisioner/BootReciever.java
@@ -0,0 +1,53 @@
+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
new file mode 100644
index 0000000..88ec454
--- /dev/null
+++ b/src/dev/oxmc/configprovisioner/ProvisioningService.java
@@ -0,0 +1,205 @@
+package dev.oxmc.configprovisioner;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.provider.Settings;
+import android.util.Log;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class ProvisioningService extends Service {
+ private static final String TAG = "ConfigProvisioner";
+ private static final String DOWNLOAD_PATH = "/data/local/tmp/config_provision.apk";
+
+ @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();
+ return START_NOT_STICKY;
+ }
+
+ new ProvisioningTask().execute();
+
+ return START_STICKY;
+ }
+
+ private class ProvisioningTask extends AsyncTask {
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ Log.i(TAG, "Starting provisioning process");
+
+ // Log all config values for debugging
+ 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;
+ }
+
+ @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
+ }
+ stopSelf();
+ }
+
+ 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 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;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Download failed", e);
+ return false;
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ 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);
+ return false;
+ }
+ }
+
+ 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 void disablePackage(String packageName) {
+ try {
+ PackageManager pm = getPackageManager();
+ pm.setApplicationEnabledSetting(packageName,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0);
+ Log.d(TAG, "Disabled package: " + packageName);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to disable package: " + packageName, e);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/dev/oxmc/configprovisioner/VendorConfig.java b/src/dev/oxmc/configprovisioner/VendorConfig.java
new file mode 100644
index 0000000..7ed9d2a
--- /dev/null
+++ b/src/dev/oxmc/configprovisioner/VendorConfig.java
@@ -0,0 +1,125 @@
+package dev.oxmc.configprovisioner;
+
+import android.util.Log;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+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";
+
+ public static boolean hasVendorConfig() {
+ File configFile = new File(VENDOR_CONFIG_PATH);
+ boolean exists = configFile.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);
+ }
+
+ public static boolean isProvisioningEnabled() {
+ return getConfigBoolean(KEY_ENABLE_PROVISIONING, DEFAULT_ENABLE_PROVISIONING);
+ }
+
+ public static String getConfigApkUrl() {
+ return getConfigString(KEY_CONFIG_APK_URL, DEFAULT_CONFIG_APK_URL);
+ }
+
+ public static String getVendorId() {
+ return getConfigString(KEY_VENDOR_ID, "default_vendor");
+ }
+
+ public static int getNetworkTimeout() {
+ try {
+ return Integer.parseInt(getConfigString(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);
+ }
+
+ // 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;
+ }
+
+ private static String getConfigValue(String key) {
+ if (!hasVendorConfig()) {
+ return null;
+ }
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(VENDOR_CONFIG_PATH))) {
+ String line;
+ while ((line = reader.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();
+ }
+ }
+ } 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