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) { 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; } 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; } @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... v) { VendorConfig.logConfigValues(); if (VendorConfig.isConfigApkServiceEnabled()) { applyConfigApkSettings(); applyApns(); } configureSetupState(); return true; } @Override protected void onPostExecute(Boolean success) { BootReceiver.setBaseProvisioned(ProvisioningService.this, true); Log.i(TAG, "Base provisioning complete"); stopSelf(); } } // ------------------------------------------------------------------------- // OTA update — runs on USER_PRESENT, deferred well after boot // ------------------------------------------------------------------------- private class OtaUpdateTask extends AsyncTask { @Override protected Boolean doInBackground(Void... v) { if (!VendorConfig.isConfigApkServiceEnabled()) { 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 { 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, "OTA update failed", e); return false; } finally { new File(DOWNLOAD_PATH).delete(); } } @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 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"); emitOemWizardSignal(pkg, res); } 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); } } /** * Signal the SetupWizard whether this config APK provides OEM wizard pages. * Writes the config APK package to Settings.Global "pawlet.oem_wizard_pkg" * when it contains res/xml/oem_wizard.xml, or an empty string otherwise. The * wizard reads this (falling back to the default config package) to decide * whether to render OEM-contributed pages. */ private void emitOemWizardSignal(String pkg, Resources res) { try { int oemId = res.getIdentifier("oem_wizard", "xml", pkg); String value = (oemId != 0) ? pkg : ""; Settings.Global.putString(getContentResolver(), "pawlet.oem_wizard_pkg", value); Log.i(TAG, "OEM wizard signal: " + (oemId != 0 ? "present (" + pkg + ")" : "none")); } catch (Exception e) { Log.w(TAG, "Failed to emit OEM wizard signal", e); } } 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 { getContentResolver().insert(Telephony.Carriers.CONTENT_URI, cv); counts[0]++; } catch (Exception e) { Log.w(TAG, "Failed to insert APN: " + name, e); } } } 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); } 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 configureSetupState() { boolean wizardEnabled = VendorConfig.isSetupWizardEnabled(); boolean deviceProvisioned = VendorConfig.isDeviceProvisioned(); Log.i(TAG, "configureSetupState: wizard=" + wizardEnabled + " device_provisioned=" + deviceProvisioned); if (wizardEnabled) { // Wizard owns the provisioning flow. Don't touch USER_SETUP_COMPLETE or // DEVICE_PROVISIONED — the wizard sets them on completion. Also don't // disable any wizard package; device_provisioned is irrelevant here. Log.i(TAG, "Setup wizard enabled: provisioning flow delegated to wizard"); } else { // Wizard is disabled. Force-set provisioned state based on device_provisioned // config so the launcher starts directly without any wizard UI. try { int v = deviceProvisioned ? 1 : 0; Settings.Secure.putInt(getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE, v); Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, v); Log.i(TAG, "Setup wizard disabled: DEVICE_PROVISIONED=" + v); } catch (Exception e) { Log.w(TAG, "Failed to set device provisioned state", e); } disablePackage("me.pawlet.setupwizard"); disablePackage("com.android.setupwizard"); disablePackage("com.google.android.setupwizard"); } } 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; } }