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.
Qué buscamos resolver
- 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.
- 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.
Cómo funciona
setForceAutoStartUpState(ON) → la tablet arranca sola con el cargador.SUCCESS en tu variante exacta de Tab A11. Por eso se valida primero en una sola unidad.Materiales
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
Generar la licencia KPE
# ~/.gradle/gradle.properties
knoxKpeLicense=KLM09-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
Knox SDK y compilación
./gradlew :app:assembleDebug
# APK: app/build/outputs/apk/debug/app-debug.apk
KnoxEnterpriseLicenseManager), el SDK cambió: avisame y ajusto el código a tu versión.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.
| Archivo | Rol |
|---|---|
kiosk/KnoxPowerManager.kt | Licencia KPE + setForceAutoStartUpState(ON) |
kiosk/KnoxLicenseReceiver.kt | Resultado de la activación de la licencia |
kiosk/BootReceiver.kt | Reaplica la política y abre Smiley al bootear |
kiosk/SmileyAdminReceiver.kt + device_admin.xml | Device Owner |
SmileyApp.kt / MainActivity.kt | Arranque de la app y modo kiosco |
AndroidManifest.xml / build.gradle.kts | Permisos, receivers, Knox SDK y clave KPE |
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.KnoxPowerManager — el corazón
Verifica Device Owner, activa la licencia KPE y llama a setForceAutoStartUpState(ON). Es idempotente: se llama en cada arranque.
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)
}
Receivers: licencia y arranque
KnoxLicenseReceiver recibe el resultado asíncrono de la licencia. BootReceiver reaplica la política y abre Smiley tras cada encendido.
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"
}
}
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"
}
}
Device Owner
Receiver de administración y políticas declaradas. Es el componente que se pasa a dpm set-device-owner.
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)
}
}
<?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>
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).
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)
}
}
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()
}
}
}
Manifest y Gradle
Permisos Knox, receivers, Smiley como HOME y la clave KPE inyectada por BuildConfig.
<?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>
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"))
}
Preparar la tablet
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}
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
Diagnóstico por mensaje de log
| Mensaje | Causa | Qué hacer |
|---|---|---|
Smiley no es Device Owner | Paso 5 no se completó | Repetir dpm set-device-owner |
Falta la clave KPE | BuildConfig vacío | Revisar gradle.properties y recompilar |
Activación … falló. errorCode=X | Clave inválida, sin internet o vencida | Verificar Wi-Fi y la clave en el portal |
setForceAutoStartUpState devolvió X | API no soportada en este firmware | Bloqueante → ir a Plan B |
SecurityException | Licencia no activa o la app no es DO | Reabrir la app; si persiste, revisar los pasos 2 y 5 |
| No aparece ningún log | No es Samsung con Knox o la app no arrancó | Revisar el Paso 1 y adb logcat completo |
Apagado manual + cargador
Descarga total de batería
Con hub USB-C + touch externo
Uso real y persistencia
¿Qué hacer con el resultado?
| Resultado | Siguiente paso |
|---|---|
| A, B y D OK | Aprobar. Pasar al despliegue: enrolamiento por QR (Android Enterprise) o Knox Mobile Enrollment para no usar cable en cada tablet. |
| Todo OK salvo C | Aprobar el autoarranque; cambiar de modelo de hub o revisar la alimentación del touch. |
| La API devuelve error | Plan 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 total | Cargador de más amperaje y alertas de batería baja desde el backend de Smiley. |