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