Lubeca Tech

Smiley · Autoarranque Tab A11 — validación

Smiley kiosk · Documento técnico de validación

Encendido automático al conectar el cargador

Checklist paso a paso para validar el módulo Knox en una tablet antes de desplegar en toda la flota.

Galaxy Tab A11Android 15Knox SDKDevice OwnerLicencia KPE
← → para navegar · los checks se guardan en este navegador
Contexto

Qué buscamos resolver

El problema Las terminales Smiley corren en tablets Samsung Galaxy Tab A11 instaladas en sitios de clientes. Si se quedan sin batería (corte de luz, cargador desconectado, falla de la fuente), se apagan.
  • Cuando vuelve la energía, la tablet solo carga: queda apagada.
  • Alguien tiene que apretar el botón de encendido.
  • Resultado: terminal fuera de servicio y visita técnica.
El objetivo Que la terminal sea autónoma frente a cortes de energía:
  • Al recibir energía, la tablet se enciende sola.
  • Al terminar de bootear, Smiley se abre sola en modo kiosco.
  • Sin depender del botón de encendido ni de una persona en el lugar.

Cómo lo vamos a hacer

Con Samsung Knox, la plataforma empresarial de Samsung, que tiene una política oficial para encender el equipo al conectarlo a la corriente. Smiley la activa por código, actuando como Device Owner de la tablet con una licencia KPE.

Alcance de este documento: validar todo el circuito en una tablet de prueba. Si pasa todas las pruebas, se aprueba el despliegue en el resto de las terminales.
Panorama

Cómo funciona

1 · Device OwnerSmiley queda como dueña del equipo. Obligatorio en Android 15 para el Knox SDK.
2 · Licencia KPESmiley activa la licencia KPE Premium (gratuita, vence a los 2 años).
3 · API KnoxsetForceAutoStartUpState(ON) → la tablet arranca sola con el cargador.
4 · BootAl bootear, Smiley se abre sola en modo kiosco.
Lo que todavía no está confirmado: que la API devuelva SUCCESS en tu variante exacta de Tab A11. Por eso se valida primero en una sola unidad.
Tiempo estimado: 1–2 h + el tiempo de descarga de batería
Paso 0

Materiales

Paso 1

Verificar modelo y versiones

En la tablet

Ajustes → Acerca de la tablet → Información de software

O por adb

# modelo y Android
adb shell getprop ro.product.model
adb shell getprop ro.build.version.release
adb shell getprop ro.hardware

# todo lo relacionado a Knox
adb shell getprop | grep -i knox
Con Android 13 o superior, la función de autoarranque de Knox es compatible con todos los chipsets.
Paso 2

Generar la licencia KPE

# ~/.gradle/gradle.properties
knoxKpeLicense=KLM09-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
Poner un recordatorio de renovación. Si la licencia vence, Knox puede dejar de aplicar políticas.
Paso 3

Knox SDK y compilación

./gradlew :app:assembleDebug
# APK: app/build/outputs/apk/debug/app-debug.apk
Si alguna constante no compila (p. ej. de KnoxEnterpriseLicenseManager), el SDK cambió: avisame y ajusto el código a tu versión.
Paso 3 · Código

El módulo de autoarranque en Smiley

Está escrito en Kotlin (corre en la misma JVM que Java y se mezcla sin problemas con código Java existente). Las 5 slides siguientes tienen cada archivo completo, con botón de copiar.

ArchivoRol
kiosk/KnoxPowerManager.ktLicencia KPE + setForceAutoStartUpState(ON)
kiosk/KnoxLicenseReceiver.ktResultado de la activación de la licencia
kiosk/BootReceiver.ktReaplica la política y abre Smiley al bootear
kiosk/SmileyAdminReceiver.kt + device_admin.xmlDevice Owner
SmileyApp.kt / MainActivity.ktArranque de la app y modo kiosco
AndroidManifest.xml / build.gradle.ktsPermisos, receivers, Knox SDK y clave KPE
Paquete usado: com.tipre.smiley. Si el applicationId real de Smiley es otro, cambiarlo en el código, el manifest, Gradle y el comando dpm del Paso 5.
Paso 3 · Código 1/5

KnoxPowerManager — el corazón

Verifica Device Owner, activa la licencia KPE y llama a setForceAutoStartUpState(ON). Es idempotente: se llama en cada arranque.

app/src/main/java/com/tipre/smiley/kiosk/KnoxPowerManager.kt
package com.tipre.smiley.kiosk

import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import com.samsung.android.knox.EnterpriseDeviceManager
import com.samsung.android.knox.custom.CustomDeviceManager
import com.samsung.android.knox.license.KnoxEnterpriseLicenseManager

/**
 * Configura el encendido automático al conectar el cargador en tablets Samsung
 * (Galaxy Tab A11 y similares) usando el Knox SDK.
 *
 * Flujo:
 *   1. Verifica que Smiley sea Device Owner (obligatorio en Android 15+).
 *   2. Activa la licencia KPE (resultado asíncrono -> KnoxLicenseReceiver).
 *   3. Con la licencia activa, llama a SystemManager.setForceAutoStartUpState(ON).
 *
 * Es idempotente: se puede llamar en cada arranque sin efectos secundarios.
 */
object KnoxPowerManager {

    private const val TAG = "KnoxPower"
    private const val PREFS = "knox_power"
    private const val KEY_LICENSE_OK = "license_ok"

    /**
     * Clave KPE generada en el Knox Admin Portal (Knox SDK > License keys).
     * Recomendado: inyectarla por BuildConfig desde gradle.properties / CI,
     * no dejarla hardcodeada en el repo.
     */
    private val KPE_LICENSE_KEY: String = BuildConfig.KNOX_KPE_LICENSE

    /** Punto de entrada: llamarlo desde Application.onCreate y desde el BootReceiver. */
    fun ensureConfigured(context: Context) {
        val ctx = context.applicationContext

        if (!isSamsungWithKnox()) {
            Log.w(TAG, "No es un equipo Samsung con Knox; se omite la configuración")
            return
        }
        if (!isDeviceOwner(ctx)) {
            Log.e(TAG, "Smiley no es Device Owner. Provisionar con: " +
                    "adb shell dpm set-device-owner ${ctx.packageName}/.kiosk.SmileyAdminReceiver")
            return
        }

        if (prefs(ctx).getBoolean(KEY_LICENSE_OK, false)) {
            applyAutoStartUp(ctx)
        } else {
            activateLicense(ctx)
        }
    }

    /** Dispara la activación de la licencia KPE. El resultado llega a KnoxLicenseReceiver. */
    fun activateLicense(context: Context) {
        if (KPE_LICENSE_KEY.isBlank()) {
            Log.e(TAG, "Falta la clave KPE (BuildConfig.KNOX_KPE_LICENSE)")
            return
        }
        try {
            KnoxEnterpriseLicenseManager.getInstance(context.applicationContext)
                .activateLicense(KPE_LICENSE_KEY)
            Log.i(TAG, "Activación de licencia KPE solicitada")
        } catch (e: Exception) {
            Log.e(TAG, "Error al solicitar la activación de la licencia KPE", e)
        }
    }

    /** Llamado por KnoxLicenseReceiver cuando la licencia se activó bien. */
    fun onLicenseActivated(context: Context) {
        prefs(context).edit().putBoolean(KEY_LICENSE_OK, true).apply()
        applyAutoStartUp(context)
    }

    /** Llamado por KnoxLicenseReceiver si la activación falló. */
    fun onLicenseFailed(context: Context, errorCode: Int) {
        prefs(context).edit().putBoolean(KEY_LICENSE_OK, false).apply()
        Log.e(TAG, "Activación de licencia KPE falló. errorCode=$errorCode")
    }

    /** Activa el encendido automático al conectar alimentación. */
    fun applyAutoStartUp(context: Context): Boolean {
        return try {
            val systemManager = CustomDeviceManager.getInstance().systemManager

            if (systemManager.forceAutoStartUpState == CustomDeviceManager.ON) {
                Log.i(TAG, "Auto start up ya estaba activo")
                return true
            }

            val result = systemManager.setForceAutoStartUpState(CustomDeviceManager.ON)
            if (result == CustomDeviceManager.SUCCESS) {
                Log.i(TAG, "Auto start up ACTIVADO: la tablet enciende sola al conectar el cargador")
                true
            } else {
                Log.e(TAG, "setForceAutoStartUpState devolvió $result")
                false
            }
        } catch (e: SecurityException) {
            // Licencia inválida/expirada o la app no es DO: se fuerza reactivación en el próximo arranque.
            Log.e(TAG, "SecurityException al aplicar auto start up", e)
            prefs(context).edit().putBoolean(KEY_LICENSE_OK, false).apply()
            false
        } catch (e: Throwable) {
            Log.e(TAG, "Error inesperado al aplicar auto start up", e)
            false
        }
    }

    /** Para desactivarlo (por ejemplo, al dar de baja una tablet). */
    fun disableAutoStartUp(): Boolean = try {
        CustomDeviceManager.getInstance().systemManager
            .setForceAutoStartUpState(CustomDeviceManager.OFF) == CustomDeviceManager.SUCCESS
    } catch (e: Throwable) {
        Log.e(TAG, "Error al desactivar auto start up", e)
        false
    }

    /** Estado actual, útil para reportarlo al backend de Smiley. */
    fun isAutoStartUpEnabled(): Boolean = try {
        CustomDeviceManager.getInstance().systemManager.forceAutoStartUpState == CustomDeviceManager.ON
    } catch (e: Throwable) {
        false
    }

    private fun isDeviceOwner(context: Context): Boolean {
        val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
        return dpm.isDeviceOwnerApp(context.packageName)
    }

    private fun isSamsungWithKnox(): Boolean {
        if (!Build.MANUFACTURER.equals("samsung", ignoreCase = true)) return false
        return try {
            EnterpriseDeviceManager.getAPILevel() > 0
        } catch (e: Throwable) {
            false
        }
    }

    private fun prefs(context: Context): SharedPreferences =
        context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
}
Paso 3 · Código 2/5

Receivers: licencia y arranque

KnoxLicenseReceiver recibe el resultado asíncrono de la licencia. BootReceiver reaplica la política y abre Smiley tras cada encendido.

kiosk/KnoxLicenseReceiver.kt
package com.tipre.smiley.kiosk

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.samsung.android.knox.license.KnoxEnterpriseLicenseManager

/**
 * Recibe el resultado asíncrono de la activación de la licencia KPE.
 */
class KnoxLicenseReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != KnoxEnterpriseLicenseManager.ACTION_LICENSE_STATUS) return

        val errorCode = intent.getIntExtra(
            KnoxEnterpriseLicenseManager.EXTRA_LICENSE_ERROR_CODE,
            KnoxEnterpriseLicenseManager.ERROR_UNKNOWN
        )
        val resultType = intent.getIntExtra(
            KnoxEnterpriseLicenseManager.EXTRA_LICENSE_RESULT_TYPE,
            -1
        )

        Log.i(TAG, "Licencia KPE: resultType=$resultType errorCode=$errorCode")

        if (resultType != KnoxEnterpriseLicenseManager.LICENSE_RESULT_TYPE_ACTIVATION) return

        if (errorCode == KnoxEnterpriseLicenseManager.ERROR_NONE) {
            KnoxPowerManager.onLicenseActivated(context)
        } else {
            KnoxPowerManager.onLicenseFailed(context, errorCode)
        }
    }

    companion object {
        private const val TAG = "KnoxLicense"
    }
}
kiosk/BootReceiver.kt
package com.tipre.smiley.kiosk

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.tipre.smiley.MainActivity

/**
 * Al terminar de bootear (incluido el arranque automático por cargador):
 *   1. Reasegura la política de Knox.
 *   2. Abre Smiley en primer plano.
 */
class BootReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            Intent.ACTION_BOOT_COMPLETED,
            Intent.ACTION_LOCKED_BOOT_COMPLETED,
            Intent.ACTION_MY_PACKAGE_REPLACED -> {
                Log.i(TAG, "Arranque detectado: ${intent.action}")
                KnoxPowerManager.ensureConfigured(context)

                val launch = Intent(context, MainActivity::class.java).apply {
                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
                }
                try {
                    context.startActivity(launch)
                } catch (e: Exception) {
                    Log.e(TAG, "No se pudo abrir Smiley tras el arranque", e)
                }
            }
        }
    }

    companion object {
        private const val TAG = "SmileyBoot"
    }
}
Paso 3 · Código 3/5

Device Owner

Receiver de administración y políticas declaradas. Es el componente que se pasa a dpm set-device-owner.

kiosk/SmileyAdminReceiver.kt
package com.tipre.smiley.kiosk

import android.app.admin.DeviceAdminReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.util.Log

/**
 * Receiver de administración del dispositivo.
 * Smiley tiene que quedar como Device Owner (DO): en Android 15+ el Knox SDK
 * solo expone sus APIs completas a una app DO o PO.
 *
 * Provisionar con:
 *   adb shell dpm set-device-owner com.tipre.smiley/.kiosk.SmileyAdminReceiver
 */
class SmileyAdminReceiver : DeviceAdminReceiver() {

    override fun onEnabled(context: Context, intent: Intent) {
        super.onEnabled(context, intent)
        Log.i(TAG, "Device admin habilitado")
        KnoxPowerManager.ensureConfigured(context)
    }

    override fun onDisabled(context: Context, intent: Intent) {
        super.onDisabled(context, intent)
        Log.w(TAG, "Device admin deshabilitado")
    }

    companion object {
        private const val TAG = "SmileyAdmin"

        fun component(context: Context): ComponentName =
            ComponentName(context.applicationContext, SmileyAdminReceiver::class.java)
    }
}
res/xml/device_admin.xml
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-policies>
        <force-lock />
        <wipe-data />
        <reset-password />
        <disable-keyguard-features />
    </uses-policies>
</device-admin>
Paso 3 · Código 4/5

App y Activity (kiosco)

SmileyApp dispara la configuración al iniciar. MainActivity entra en lock task y mantiene la pantalla encendida (reemplazar por la UI real de Smiley).

SmileyApp.kt
package com.tipre.smiley

import android.app.Application
import com.tipre.smiley.kiosk.KnoxPowerManager

class SmileyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        KnoxPowerManager.ensureConfigured(this)
    }
}
MainActivity.kt
package com.tipre.smiley

import android.app.Activity
import android.app.admin.DevicePolicyManager
import android.content.Context
import android.os.Bundle
import android.view.WindowManager
import com.tipre.smiley.kiosk.SmileyAdminReceiver

/**
 * Activity principal de Smiley (reemplazar el contenido por la UI real).
 * Al ser Device Owner, se auto-habilita para lock task (modo kiosco)
 * y mantiene la pantalla encendida.
 */
class MainActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
        // setContentView(R.layout.activity_main)  // UI de Smiley
    }

    override fun onResume() {
        super.onResume()
        enterKioskIfPossible()
    }

    private fun enterKioskIfPossible() {
        val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
        if (!dpm.isDeviceOwnerApp(packageName)) return

        val admin = SmileyAdminReceiver.component(this)
        dpm.setLockTaskPackages(admin, arrayOf(packageName))
        // Smiley como launcher persistente: tras cualquier reinicio vuelve a esta app.
        dpm.setLockTaskFeatures(admin, DevicePolicyManager.LOCK_TASK_FEATURE_NONE)
        if (dpm.isLockTaskPermitted(packageName)) {
            startLockTask()
        }
    }
}
Paso 3 · Código 5/5

Manifest y Gradle

Permisos Knox, receivers, Smiley como HOME y la clave KPE inyectada por BuildConfig.

app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- Knox: licencia KPE y APIs de customización de sistema -->
    <uses-permission android:name="com.samsung.android.knox.permission.KNOX_CUSTOM_SYSTEM" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:name=".SmileyApp"
        android:label="Smiley"
        android:allowBackup="false">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTask"
            android:lockTaskMode="if_whitelisted"
            android:screenOrientation="sensorLandscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
                <!-- Smiley como home: si algo la cierra, Android vuelve a abrirla -->
                <category android:name="android.intent.category.HOME" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <!-- Device Owner -->
        <receiver
            android:name=".kiosk.SmileyAdminReceiver"
            android:exported="true"
            android:permission="android.permission.BIND_DEVICE_ADMIN">
            <meta-data
                android:name="android.app.device_admin"
                android:resource="@xml/device_admin" />
            <intent-filter>
                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
                <action android:name="android.app.action.PROFILE_PROVISIONING_COMPLETE" />
            </intent-filter>
        </receiver>

        <!-- Resultado de la activación de licencia KPE -->
        <receiver
            android:name=".kiosk.KnoxLicenseReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="com.samsung.android.knox.intent.action.KNOX_LICENSE_STATUS" />
            </intent-filter>
        </receiver>

        <!-- Arranque -->
        <receiver
            android:name=".kiosk.BootReceiver"
            android:exported="true"
            android:directBootAware="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
            </intent-filter>
        </receiver>
    </application>
</manifest>
app/build.gradle.kts
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.tipre.smiley"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.tipre.smiley"
        minSdk = 30
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"

        // Clave KPE: definir knoxKpeLicense en ~/.gradle/gradle.properties o en CI
        val kpe = (project.findProperty("knoxKpeLicense") as String?) ?: ""
        buildConfigField("String", "KNOX_KPE_LICENSE", "\"$kpe\"")
    }

    buildFeatures {
        buildConfig = true
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
}

dependencies {
    // Knox SDK: descargar knoxsdk.jar desde el Samsung Knox developer portal y copiarlo en app/libs.
    // compileOnly: las clases las provee el firmware de Samsung en runtime.
    compileOnly(files("libs/knoxsdk.jar"))
}
Paso 4

Preparar la tablet

Si se agrega cualquier cuenta, Android no permite asignar Device Owner. La única salida es otro reset.
Paso 5

Instalar y asignar Device Owner

adb install -r app-debug.apk
adb shell dpm set-device-owner com.tipre.smiley/.kiosk.SmileyAdminReceiver

Salida esperada

Success: Device owner set to package com.tipre.smiley
Active admin set to component {com.tipre.smiley/com.tipre.smiley.kiosk.SmileyAdminReceiver}
"Not allowed to set the device owner because there are already some accounts" → hay una cuenta agregada: volver al Paso 4.
Paso 6

Abrir Smiley y leer el log

# terminal 1
adb logcat -c && adb logcat -s KnoxPower KnoxLicense SmileyBoot

# terminal 2
adb shell am start -n com.tipre.smiley/.MainActivity

Secuencia esperada

KnoxPower   : Activación de licencia KPE solicitada
KnoxLicense : Licencia KPE: resultType=… errorCode=0
KnoxPower   : Auto start up ACTIVADO: la tablet enciende sola al conectar el cargador
Paso 6 · Si algo falla

Diagnóstico por mensaje de log

MensajeCausaQué hacer
Smiley no es Device OwnerPaso 5 no se completóRepetir dpm set-device-owner
Falta la clave KPEBuildConfig vacíoRevisar gradle.properties y recompilar
Activación … falló. errorCode=XClave inválida, sin internet o vencidaVerificar Wi-Fi y la clave en el portal
setForceAutoStartUpState devolvió XAPI no soportada en este firmwareBloqueante → ir a Plan B
SecurityExceptionLicencia no activa o la app no es DOReabrir la app; si persiste, revisar los pasos 2 y 5
No aparece ningún logNo es Samsung con Knox o la app no arrancóRevisar el Paso 1 y adb logcat completo
Paso 7 · Prueba A

Apagado manual + cargador

Si en vez de arrancar muestra el ícono de batería cargando, la política no quedó aplicada: volver al Paso 6.
Paso 8 · Prueba B

Descarga total de batería

Si entra en loop, probar con un cargador de más amperaje. Samsung documenta este comportamiento cuando la batería queda totalmente agotada mucho tiempo.
Paso 9 · Prueba C (opcional)

Con hub USB-C + touch externo

Si con el hub no arranca sola pero sin hub sí, el problema es del hub (no deja pasar la energía con el host apagado): probar otro modelo.
Paso 10 · Prueba D

Uso real y persistencia

Decisión

¿Qué hacer con el resultado?

ResultadoSiguiente paso
A, B y D OKAprobar. Pasar al despliegue: enrolamiento por QR (Android Enterprise) o Knox Mobile Enrollment para no usar cable en cada tablet.
Todo OK salvo CAprobar el autoarranque; cambiar de modelo de hub o revisar la alimentación del touch.
La API devuelve errorPlan B (siguiente slide). Probar primero la política "Set Power ON when connected to power source" de Knox Service Plugin desde un MDM.
Loop en descarga totalCargador de más amperaje y alertas de batería baja desde el backend de Smiley.
Plan B

Si la API no funciona en esta tablet

MDM con KnoxKnox Manage / Hexnode / SOTI con la política "Power ON when connected". Misma función desde una consola, con costo por equipo.
Que nunca se apagueMini-UPS o power bank con pass-through + alerta de batería baja desde Smiley.
HardwareMicrocontrolador que detecta los 5V del USB y simula el botón de encendido. Funciona siempre, pero se pierde la garantía.
Otro equipoPanel Android industrial (Rockchip) con "auto power on" de fábrica.
Descartado: root / desbloquear el bootloader. Quema el e-fuse de Knox y elimina para siempre la opción principal.
Registro

Hoja de la prueba

⎙ Imprimir para guardar en PDF