A reusable Android library for Samsung Knox Enterprise License Management that provides a clean, coroutine-based API for license activation, deactivation, and monitoring.
- Features
- Installation
- Knox License Setup
- Usage
- Configuration
- Dependency Injection Integration
- API Reference
- Requirements
- Error Handling
- Security Considerations
- Performance Considerations
- Troubleshooting
- License
- Clean Architecture: Separation of domain and data layers
- Coroutines Support: Async/await license operations with Flow-based state monitoring
- Named License Keys: Support for multiple named license configurations
- Configurable License Selection: Pluggable strategy pattern for custom license selection logic
- Device-Agnostic: No hardcoded device detection dependencies
- Comprehensive Error Handling: Detailed Knox SDK error code mapping
- Framework Agnostic: No dependency injection framework required
- Easy Integration: Simple factory pattern for instantiation
Add this module as a git submodule to your Android project:
git submodule add https://github.com/jpicklyk/knox-licensing.git knox-licensingInclude the module in your settings.gradle.kts:
include(":knox-licensing")Add the dependency in your app's build.gradle.kts:
dependencies {
implementation(project(":knox-licensing"))
}Before using Knox policies on a device, you need a Knox Platform for Enterprise (KPE) license:
-
Create a Samsung Knox Developer account at developer.samsungknox.com
-
Obtain a KPE license key — follow the Knox Platform for Enterprise license guide to generate your license key
-
Add the license to
local.propertiesin the project root:knox.license=KLM09-XXXX...XXX -
Set a unique package name — your application's package name must be unique and match what is registered with your license. Use the rename scripts to update the package name across the codebase:
- Windows:
.\UpdatePackageName.ps1 - Mac/Linux:
./update_package_name.sh
- Windows:
-
Bind the application to your license — the compiled application must be bound to your license key in the Knox console for the activation process to succeed. Register your app's package name and signing certificate in your Knox developer portal.
import com.github.jpicklyk.knox.licensing.KnoxLicenseFactory
import com.github.jpicklyk.knox.licensing.domain.KnoxLicenseHandler
import com.github.jpicklyk.knox.licensing.domain.LicenseResult
import com.github.jpicklyk.knox.licensing.domain.LicenseState
import com.github.jpicklyk.knox.licensing.domain.LicenseSelectionStrategy
class MainActivity : AppCompatActivity() {
private lateinit var knoxLicenseHandler: KnoxLicenseHandler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create handler from BuildConfig (uses default license key)
knoxLicenseHandler = KnoxLicenseFactory.createFromBuildConfig(this)
// Create with custom license selection strategy
val customStrategy = MyLicenseSelectionStrategy()
knoxLicenseHandler = KnoxLicenseFactory.create(this, customStrategy)
// Create with app's BuildConfig (when used in multi-module projects)
knoxLicenseHandler = KnoxLicenseFactory.create(
context = this,
licenseSelectionStrategy = customStrategy,
defaultKey = BuildConfig.KNOX_LICENSE_KEY,
namedKeysArray = BuildConfig.KNOX_LICENSE_KEYS
)
// Or create with explicit keys
knoxLicenseHandler = KnoxLicenseFactory.createWithKeys(
context = this,
defaultKey = "KLM06-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
namedKeys = mapOf(
"production" to "KLM06-YYYYY-YYYYY-YYYYY-YYYYY-YYYYY",
"enterprise" to "KLM06-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ"
)
)
}
}The library supports custom license selection logic through the LicenseSelectionStrategy interface. This allows applications to implement device-specific or context-specific license selection without creating dependencies.
import android.os.Build
import android.content.pm.PackageManager
import com.github.jpicklyk.knox.licensing.domain.LicenseSelectionStrategy
class MyDeviceBasedStrategy : LicenseSelectionStrategy {
override fun selectLicenseKey(availableKeys: Map<String, String>, defaultKey: String): String {
return when {
isProductionDevice() -> availableKeys["production"] ?: defaultKey
isEnterpriseDevice() -> availableKeys["enterprise"] ?: defaultKey
else -> defaultKey
}
}
private fun isProductionDevice(): Boolean {
// Example production device detection logic
return Build.MODEL.contains("PROD") ||
Build.DISPLAY.contains("RELEASE") ||
!BuildConfig.DEBUG
}
private fun isEnterpriseDevice(): Boolean {
// Example enterprise device detection logic
return try {
// Check for enterprise features or specific device configurations
Build.MODEL.contains("ENTERPRISE") ||
Build.BRAND.equals("SAMSUNG", ignoreCase = true)
} catch (e: Exception) {
false
}
}
}
// Use the strategy
val strategy = MyDeviceBasedStrategy()
val handler = KnoxLicenseFactory.create(context, strategy)- Flexibility: Implement any device detection or selection logic
- Testability: Easy to unit test different selection scenarios
- Independence: No dependencies on specific device detection libraries
- Reusability: Same strategy can be used across different apps
When your project uses a custom Knox SDK module (e.g., knox-custom-sdk-module) that requires a different license than the standard commercial license, implement a device-aware strategy:
/**
* License selection strategy that chooses between commercial and custom SDK licenses
* based on device characteristics.
*/
class CustomSdkLicenseSelectionStrategy : LicenseSelectionStrategy {
override fun selectLicenseKey(availableKeys: Map<String, String>, defaultKey: String): String {
return if (isCustomSdkDevice()) {
// Use custom SDK license for specialized devices
availableKeys["custom-sdk"] ?: defaultKey
} else {
// Use standard commercial license for regular devices
defaultKey
}
}
/**
* Detect if device requires custom SDK license.
* Implement your device-specific detection logic here.
*/
private fun isCustomSdkDevice(): Boolean {
// Example: Check for specific device models or configurations
val model = Build.MODEL.uppercase()
return model.contains("CUSTOM") ||
model.contains("SPECIALIZED") ||
hasCustomSdkFeature()
}
private fun hasCustomSdkFeature(): Boolean {
// Check for device-specific features that indicate custom SDK requirement
return try {
// Example: Check system property or feature flag
val prop = System.getProperty("ro.custom.sdk.enabled", "false")
prop.equals("true", ignoreCase = true)
} catch (e: Exception) {
false
}
}
}
// Provide via Hilt DI
@Module
@InstallIn(SingletonComponent::class)
object AppLicensingModule {
@Provides
@Singleton
fun provideLicenseSelectionStrategy(): LicenseSelectionStrategy {
return CustomSdkLicenseSelectionStrategy()
}
}With the corresponding local.properties configuration:
# Standard commercial Knox license
knox.license=KLM06-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
# Custom SDK license for specialized devices
knox.license.custom-sdk=KLM09-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX// Activate default license
lifecycleScope.launch {
when (val result = knoxLicenseHandler.activate()) {
is LicenseResult.Success -> {
Log.d("Knox", "License activated: ${result.message}")
}
is LicenseResult.Error -> {
Log.e("Knox", "Activation failed: ${result.message}")
}
}
}
// Activate named license
lifecycleScope.launch {
val result = knoxLicenseHandler.activate("production")
// Handle result...
}
// Deactivate license
lifecycleScope.launch {
val result = knoxLicenseHandler.deactivate()
// Handle result...
}
// Get license information
lifecycleScope.launch {
val licenseInfo = knoxLicenseHandler.getLicenseInfo()
Log.d("Knox", "License active: ${licenseInfo.isActivated}")
}// Observe license state changes
lifecycleScope.launch {
knoxLicenseHandler.observeLicenseState().collect { state ->
when (state) {
is LicenseState.Loading -> {
// Show loading indicator
}
is LicenseState.Activated -> {
Log.d("Knox", "License activated: ${state.message}")
}
is LicenseState.Deactivated -> {
Log.d("Knox", "License deactivated: ${state.message}")
}
is LicenseState.Error -> {
Log.e("Knox", "License error: ${state.message}")
}
}
}
}// Check available licenses
val availableLicenses = knoxLicenseHandler.getAvailableLicenses()
availableLicenses.forEach { (name, key) ->
Log.d("Knox", "License '$name': ${key.take(10)}...")
}
// Check if specific license exists
if (knoxLicenseHandler.hasLicense("production")) {
// Production license is configured
}The library provides two approaches for license initialization:
KnoxLicenseInitializer is a class that can be injected via Hilt or other DI frameworks:
@HiltAndroidApp
class MyApplication : Application() {
@Inject
lateinit var licenseInitializer: KnoxLicenseInitializer
override fun onCreate() {
super.onCreate()
lifecycleScope.launch {
val result = licenseInitializer.initialize(this@MyApplication)
// Handle result...
}
}
}
// In ViewModels, observe the reactive status
@HiltViewModel
class MainViewModel @Inject constructor(
private val licenseInitializer: KnoxLicenseInitializer
) : ViewModel() {
val licenseStatus: StateFlow<LicenseStartupResult> = licenseInitializer.licenseStatus
}Note: If using knox-hilt,
KnoxLicenseInitializeris automatically provided byKnoxLicensingModule.
KnoxStartupManager provides static methods for convenient initialization without DI:
import com.github.jpicklyk.knox.licensing.domain.KnoxStartupManager
import com.github.jpicklyk.knox.licensing.domain.LicenseStartupResult
import com.github.jpicklyk.knox.licensing.domain.LicenseSelectionStrategy
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Initialize Knox licensing at startup with default configuration
lifecycleScope.launch {
when (val result = KnoxStartupManager.initializeKnoxLicensing(this@MyApplication)) {
// Handle results...
}
}
// Or initialize with custom license selection strategy
lifecycleScope.launch {
val customStrategy = MyDeviceBasedStrategy()
when (val result = KnoxStartupManager.initializeKnoxLicensing(this@MyApplication, customStrategy)) {
// Handle results...
}
}
// Or initialize with app's BuildConfig (for multi-module projects)
lifecycleScope.launch {
val customStrategy = MyDeviceBasedStrategy()
when (val result = KnoxStartupManager.initializeKnoxLicensing(
context = this@MyApplication,
licenseSelectionStrategy = customStrategy,
defaultKey = BuildConfig.KNOX_LICENSE_KEY,
namedKeysArray = BuildConfig.KNOX_LICENSE_KEYS
)) {
is LicenseStartupResult.AlreadyActivated -> {
Log.d("Knox", "License was already activated")
}
is LicenseStartupResult.ActivatedNow -> {
Log.d("Knox", "License activated successfully")
}
is LicenseStartupResult.ActivationFailed -> {
Log.e("Knox", "License activation failed: ${result.reason}")
}
is LicenseStartupResult.InitializationError -> {
Log.e("Knox", "Initialization error: ${result.reason}")
}
LicenseStartupResult.NotChecked -> {
Log.w("Knox", "License status not checked")
}
}
}
}
}// Check if Knox licensing is ready for use
if (KnoxStartupManager.isKnoxLicenseReady()) {
// Proceed with Knox operations
}
// Get current license status
val status = KnoxStartupManager.getLicenseStatus()
// Reset startup manager (useful for testing)
KnoxStartupManager.reset()The startup manager automatically:
- Uses custom license selection strategy if provided
- Checks if license is already activated before attempting activation
- Provides detailed status reporting
- Handles initialization errors gracefully
- Uses singleton pattern to avoid duplicate initialization
For multi-module Android projects, creating a Gradle convention plugin is the cleanest approach. This plugin reads license keys from local.properties and injects them into your app's BuildConfig.
Create KnoxLicenseConventionPlugin.kt in your build-logic/convention/src/main/kotlin/:
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import java.util.Properties
class KnoxLicenseConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
when {
plugins.hasPlugin("com.android.application") -> {
extensions.configure<ApplicationExtension> {
buildFeatures { buildConfig = true }
defaultConfig {
val defaultKey = getKnoxLicenseKey()
val namedKeys = getNamedLicenseKeys()
// Default/commercial license key
buildConfigField("String", "KNOX_LICENSE_KEY", "\"$defaultKey\"")
// Array of named keys in "name:key" format for LicenseKeyProvider
buildConfigField("String[]", "KNOX_LICENSE_KEYS", namedKeys.toArrayLiteral())
}
}
}
plugins.hasPlugin("com.android.library") -> {
// Similar configuration for library modules
}
}
}
}
private fun Project.getKnoxLicenseKey(): String {
return getPropertyFromLocalProperties("knox.license", "KNOX_LICENSE_KEY_NOT_FOUND")
}
/**
* Reads all knox.license.* properties and returns them as name:key pairs.
* Example: knox.license.custom-sdk=KEY123 -> "custom-sdk:KEY123"
*/
private fun Project.getNamedLicenseKeys(): List<String> {
val localProperties = Properties()
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { localProperties.load(it) }
}
return localProperties.entries
.filter { (key, _) ->
key.toString().startsWith("knox.license.") &&
key.toString() != "knox.license"
}
.map { (key, value) ->
val name = key.toString().removePrefix("knox.license.")
"$name:$value"
}
}
private fun List<String>.toArrayLiteral(): String {
return if (isEmpty()) "{}" else joinToString(prefix = "{", postfix = "}") { "\"$it\"" }
}
private fun Project.getPropertyFromLocalProperties(key: String, defaultValue: String): String {
val localProperties = Properties()
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { localProperties.load(it) }
}
return localProperties.getProperty(key, defaultValue)
}
}In your build-logic/convention/build.gradle.kts:
gradlePlugin {
plugins {
register("knoxLicense") {
id = "convention.android.knox.license"
implementationClass = "KnoxLicenseConventionPlugin"
}
}
}In your app module's build.gradle.kts:
plugins {
id("com.android.application")
id("convention.android.knox.license") // Apply the Knox license plugin
}Add to your local.properties (never commit to version control):
# Default/commercial license key
knox.license=KLM06-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
# Named license keys (any knox.license.* property is automatically picked up)
knox.license.custom-sdk=KLM09-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
knox.license.enterprise=KLM06-YYYYY-YYYYY-YYYYY-YYYYY-YYYYY
knox.license.development=KLM06-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZThe plugin automatically discovers all knox.license.* properties and generates the KNOX_LICENSE_KEYS array in the format expected by LicenseKeyProvider:
knox.license.custom-sdk→"custom-sdk:KLM09-..."knox.license.enterprise→"enterprise:KLM06-..."
This approach ensures license keys are:
- Centrally managed in
local.properties - Automatically injected into BuildConfig
- Available for license selection strategies
- Never committed to version control
When deploying to multiple customers who each require their own Knox license, use Android product flavors to manage customer-specific configurations.
In your app's build.gradle.kts:
android {
flavorDimensions += "customer"
productFlavors {
create("customerA") {
dimension = "customer"
applicationIdSuffix = ".customera"
}
create("customerB") {
dimension = "customer"
applicationIdSuffix = ".customerb"
}
create("internal") {
dimension = "customer"
// Internal/development builds
}
}
}In your local.properties:
# Default/fallback license (used if flavor-specific key not found)
knox.license=KLM06-DEFAULT-XXXXX-XXXXX-XXXXX-XXXXX
# Customer-specific license keys
knox.license.customerA=KLM06-CUSTA-XXXXX-XXXXX-XXXXX-XXXXX
knox.license.customerB=KLM06-CUSTB-XXXXX-XXXXX-XXXXX-XXXXX
knox.license.internal=KLM06-INTERNAL-XXXXX-XXXXX-XXXXX
# Customer + device-specific keys (for custom SDK devices per customer)
knox.license.customerA.custom-sdk=KLM09-CUSTA-CUSTOM-XXXXX-XXXXX
knox.license.customerB.custom-sdk=KLM09-CUSTB-CUSTOM-XXXXX-XXXXXExtend your convention plugin to configure flavor-specific BuildConfig fields:
private fun Project.configureApplicationBuildConfig(extension: ApplicationExtension) {
extension.apply {
buildFeatures { buildConfig = true }
// Default configuration (fallback)
defaultConfig {
val defaultKey = getKnoxLicenseKey("knox.license")
buildConfigField("String", "KNOX_LICENSE_KEY", "\"$defaultKey\"")
buildConfigField("String[]", "KNOX_LICENSE_KEYS", getNamedLicenseKeys().toArrayLiteral())
}
// Configure each product flavor with its specific license
productFlavors.configureEach {
val flavorName = this.name
val flavorKey = getKnoxLicenseKey("knox.license.$flavorName")
val flavorNamedKeys = getNamedLicenseKeysForFlavor(flavorName)
if (flavorKey != "KNOX_LICENSE_KEY_NOT_FOUND") {
buildConfigField("String", "KNOX_LICENSE_KEY", "\"$flavorKey\"")
}
if (flavorNamedKeys.isNotEmpty()) {
buildConfigField("String[]", "KNOX_LICENSE_KEYS", flavorNamedKeys.toArrayLiteral())
}
}
}
}
/**
* Gets named keys for a specific flavor.
* Looks for: knox.license.<flavorName>.<keyName>=VALUE
*/
private fun Project.getNamedLicenseKeysForFlavor(flavorName: String): List<String> {
val prefix = "knox.license.$flavorName."
return loadLocalProperties().entries
.filter { (key, _) -> key.toString().startsWith(prefix) }
.map { (key, value) ->
val keyName = key.toString().removePrefix(prefix)
"$keyName:$value"
}
}Create a strategy that considers both customer (flavor) and device type:
class CustomerAwareLicenseStrategy : LicenseSelectionStrategy {
override fun selectLicenseKey(availableKeys: Map<String, String>, defaultKey: String): String {
// The defaultKey is already customer-specific (set by flavor's BuildConfig)
// availableKeys contains customer-specific named keys (e.g., "custom-sdk")
return when {
isCustomSdkDevice() -> availableKeys["custom-sdk"] ?: defaultKey
else -> defaultKey
}
}
private fun isCustomSdkDevice(): Boolean {
// Device-specific detection logic
return Build.MODEL.contains("CUSTOM")
}
}With this setup, your build variants would be:
| Variant | License Source | Use Case |
|---|---|---|
customerADebug |
knox.license.customerA |
Customer A development |
customerARelease |
knox.license.customerA |
Customer A production |
customerBDebug |
knox.license.customerB |
Customer B development |
customerBRelease |
knox.license.customerB |
Customer B production |
internalDebug |
knox.license.internal |
Internal testing |
For CI/CD pipelines, inject customer-specific keys via environment variables:
# In CI pipeline
echo "knox.license.customerA=$CUSTOMER_A_LICENSE" >> local.properties
echo "knox.license.customerB=$CUSTOMER_B_LICENSE" >> local.properties
# Build specific customer variant
./gradlew assembleCustomerARelease- Isolate customer keys: Each customer's license should only be in their specific build variant
- CI/CD secrets: Store customer keys as separate CI/CD secrets, injected only for their builds
- Code signing: Use different signing keys per customer to prevent APK tampering
- License auditing: Log license activation (without the key) for compliance tracking
Alternatively, add BuildConfig fields directly to your build.gradle.kts:
android {
defaultConfig {
buildConfigField("String", "KNOX_LICENSE_KEY", getDefaultLicenseKey())
buildConfigField("String[]", "KNOX_LICENSE_KEYS", getNamedLicenseKeys())
}
buildFeatures {
buildConfig = true
}
}
fun getDefaultLicenseKey(): String {
// Manually load local.properties since project.findProperty() doesn't auto-load it
val localPropsFile = File(project.rootDir, "local.properties")
val localProps = Properties()
if (localPropsFile.exists()) {
localPropsFile.inputStream().use { localProps.load(it) }
}
val defaultKey = localProps.getProperty("knox.license") ?: ""
return "\"$defaultKey\""
}
fun getNamedLicenseKeys(): String {
// Manually load local.properties
val localPropsFile = File(project.rootDir, "local.properties")
val localProps = Properties()
if (localPropsFile.exists()) {
localPropsFile.inputStream().use { localProps.load(it) }
}
val keys = mutableListOf<String>()
// Add named keys from properties
localProps.getProperty("knox.license.production")?.let { key ->
keys.add("\"production:$key\"")
}
localProps.getProperty("knox.license.enterprise")?.let { key ->
keys.add("\"enterprise:$key\"")
}
return if (keys.isEmpty()) {
"new String[]{}"
} else {
"new String[]{${keys.joinToString(", ")}}"
}
}Add to your local.properties:
knox.license=KLM06-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
knox.license.production=KLM09-YYYYY-YYYYY-YYYYY-YYYYY-YYYYY
knox.license.enterprise=KLM06-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZImportant Note: Due to how Gradle handles project.findProperty(), it doesn't automatically load local.properties. The functions above manually load the properties file to ensure BuildConfig generation works correctly.
When using this library in multi-module Android projects, there are two configuration approaches:
The knox-licensing module reads from its own BuildConfig. Configure license keys in the knox-licensing module's build.gradle.kts:
// In knox-licensing/build.gradle.kts
android {
defaultConfig {
buildConfigField("String", "KNOX_LICENSE_KEY", getDefaultLicenseKey())
buildConfigField("String[]", "KNOX_LICENSE_KEYS", getNamedLicenseKeys())
}
}Then use the standard factory methods:
val handler = KnoxLicenseFactory.create(context, customStrategy)The app module configures license keys and passes them to knox-licensing. This is the recommended approach for better separation of concerns:
// In app/build.gradle.kts
android {
defaultConfig {
buildConfigField("String", "KNOX_LICENSE_KEY", getDefaultLicenseKey())
buildConfigField("String[]", "KNOX_LICENSE_KEYS", getNamedLicenseKeys())
}
}
// In your app code
val handler = KnoxLicenseFactory.create(
context = context,
licenseSelectionStrategy = customStrategy,
defaultKey = BuildConfig.KNOX_LICENSE_KEY,
namedKeysArray = BuildConfig.KNOX_LICENSE_KEYS
)
// Or with KnoxStartupManager
KnoxStartupManager.initializeKnoxLicensing(
context = context,
licenseSelectionStrategy = customStrategy,
defaultKey = BuildConfig.KNOX_LICENSE_KEY,
namedKeysArray = BuildConfig.KNOX_LICENSE_KEYS
)This approach ensures that license keys are managed centrally in the app module while the knox-licensing library remains configuration-agnostic.
The library supports custom license selection through the strategy pattern:
- Default Behavior: Uses
knox.licensefrom BuildConfig - Custom Strategy: Implement
LicenseSelectionStrategyfor device-specific logic - Named Keys: Access configured named keys through the strategy interface
This allows applications to implement their own device detection and license selection logic without creating dependencies on specific device libraries.
val licenseConfiguration = LicenseConfiguration(
defaultKey = "KLM06-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
namedKeys = mapOf(
"production" to "KLM06-YYYYY-YYYYY-YYYYY-YYYYY-YYYYY",
"enterprise" to "KLM06-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ"
)
)
val knoxLicenseHandler = KnoxLicenseFactory.create(context, licenseConfiguration)The library is framework-agnostic and doesn't include any DI dependencies. You can easily integrate it with your preferred DI framework:
If using knox-hilt, KnoxLicenseInitializer is automatically provided. You only need to provide your LicenseSelectionStrategy:
@Module
@InstallIn(SingletonComponent::class)
object AppLicensingModule {
@Provides
@Singleton
fun provideLicenseSelectionStrategy(): LicenseSelectionStrategy {
return MyDeviceBasedStrategy()
}
}
// Usage in your components
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var licenseInitializer: KnoxLicenseInitializer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
licenseInitializer.initialize(this@MainActivity)
}
}
}If not using knox-hilt, provide the full module yourself:
@Module
@InstallIn(SingletonComponent::class)
object KnoxLicenseModule {
@Provides
@Singleton
fun provideLicenseSelectionStrategy(): LicenseSelectionStrategy {
return MyDeviceBasedStrategy()
}
@Provides
@Singleton
fun provideKnoxLicenseInitializer(): KnoxLicenseInitializer {
return KnoxLicenseInitializer().also {
// Register for backward compatibility with KnoxStartupManager
KnoxStartupManager.setInstance(it)
}
}
@Provides
@Singleton
fun provideKnoxLicenseHandler(
@ApplicationContext context: Context,
licenseSelectionStrategy: LicenseSelectionStrategy
): KnoxLicenseHandler {
return KnoxLicenseFactory.create(context, licenseSelectionStrategy)
}
}val knoxLicenseModule = module {
single<LicenseSelectionStrategy> { MyDeviceBasedStrategy() }
single<KnoxLicenseHandler> {
KnoxLicenseFactory.create(androidContext(), get())
}
}
// In your Application class
startKoin {
androidContext(this@MyApplication)
modules(knoxLicenseModule)
}
// Usage in your components
class MainActivity : AppCompatActivity() {
private val knoxLicenseHandler: KnoxLicenseHandler by inject()
// Use knoxLicenseHandler...
}The main interface for Knox license management operations.
interface KnoxLicenseHandler {
// License activation operations
suspend fun activate(licenseName: String = "default"): LicenseResult
suspend fun deactivate(licenseName: String = "default"): LicenseResult
// License information
suspend fun getLicenseInfo(): LicenseInfo
fun getAvailableLicenses(): Map<String, String>
fun hasLicense(licenseName: String): Boolean
// State monitoring
fun observeLicenseState(): Flow<LicenseState>
}Interface for implementing custom license selection logic.
interface LicenseSelectionStrategy {
/**
* Selects appropriate license key based on available keys and device context
* @param availableKeys Map of named license keys (name -> key)
* @param defaultKey The default license key to fall back to
* @return Selected license key string
*/
fun selectLicenseKey(availableKeys: Map<String, String>, defaultKey: String): String
}Sealed class representing the result of license operations.
sealed class LicenseResult {
data class Success(val message: String) : LicenseResult()
data class Error(val message: String, val errorCode: Int = -1) : LicenseResult()
}Sealed class representing the current state of license.
sealed class LicenseState {
object Loading : LicenseState()
data class Activated(val message: String) : LicenseState()
data class Deactivated(val message: String) : LicenseState()
data class Error(val message: String) : LicenseState()
}Data class containing license information.
data class LicenseInfo(
val isActivated: Boolean,
val licenseKey: String? = null,
val activationDate: String? = null,
val expirationDate: String? = null,
val errorCode: Int? = null,
val errorMessage: String? = null
)Data class for license configuration.
data class LicenseConfiguration(
val defaultKey: String,
val namedKeys: Map<String, String> = emptyMap()
) {
fun getKey(licenseName: String): String
fun getAllKeyNames(): Set<String>
}Factory class for creating KnoxLicenseHandler instances.
object KnoxLicenseFactory {
// Create from BuildConfig
fun createFromBuildConfig(context: Context): KnoxLicenseHandler
// Create with license selection strategy
fun create(context: Context, licenseSelectionStrategy: LicenseSelectionStrategy?): KnoxLicenseHandler
// Create with app BuildConfig (multi-module)
fun create(
context: Context,
licenseSelectionStrategy: LicenseSelectionStrategy?,
defaultKey: String,
namedKeysArray: Array<String>?
): KnoxLicenseHandler
// Create with explicit configuration
fun create(context: Context, licenseConfiguration: LicenseConfiguration): KnoxLicenseHandler
// Create with explicit keys
fun createWithKeys(
context: Context,
defaultKey: String,
namedKeys: Map<String, String> = emptyMap()
): KnoxLicenseHandler
}Utility for managing Knox license initialization during app startup.
object KnoxStartupManager {
// Initialize with default configuration
suspend fun initializeKnoxLicensing(context: Context): LicenseStartupResult
// Initialize with custom strategy
suspend fun initializeKnoxLicensing(
context: Context,
licenseSelectionStrategy: LicenseSelectionStrategy?
): LicenseStartupResult
// Initialize with app BuildConfig
suspend fun initializeKnoxLicensing(
context: Context,
licenseSelectionStrategy: LicenseSelectionStrategy?,
defaultKey: String,
namedKeysArray: Array<String>?
): LicenseStartupResult
// Status checking
fun isKnoxLicenseReady(): Boolean
fun getLicenseStatus(): LicenseStartupResult
fun reset()
}- Android API 21+
- Samsung Knox-enabled device
- Knox Enterprise License Manager
- Device Administrator or Device Owner/Profile Owner privileges (required for license operations)
The module requires the following dependencies. If you're creating your own libs.versions.toml, use these naming conventions:
[versions]
androidx-core = "1.15.0" # Or latest stable
kotlinx-coroutines = "1.10.2" # Or latest stable
junit4 = "4.13.2"
mockk-version = "1.14.7"
testing-androidx-junit = "1.2.1"
testing-androidx-espresso = "3.6.1"
[libraries]
# Required runtime dependencies
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
# Testing dependencies (optional)
junit = { group = "junit", name = "junit", version.ref = "junit4" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
mockk-core = { group = "io.mockk", name = "mockk", version.ref = "mockk-version" }
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "testing-androidx-junit" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "testing-androidx-espresso" }The module expects a Knox Enterprise SDK JAR file at libs/knoxsdk_ver38.jar. This is included as compileOnly to avoid conflicts when other modules provide their own SDK variant.
// In knox-licensing/build.gradle.kts
dependencies {
// Knox SDK - compileOnly so consumers provide their own SDK JAR
compileOnly(files("libs/knoxsdk_ver38.jar"))
// Runtime dependencies
implementation(libs.androidx.core.ktx)
implementation(libs.kotlinx.coroutines.android)
// Testing
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk.core)
androidTestImplementation(libs.androidx.test.junit)
androidTestImplementation(libs.androidx.test.espresso.core)
}Note: The consuming application must provide the Knox SDK JAR either directly or through another module that includes it.
Knox license activation requires elevated permissions. Your application must be registered as one of the following:
-
Device Administrator (DA): Application registered via Device Administration API
- Requires user consent to enable
- Suitable for enterprise applications with user interaction
- Can be disabled by users in Settings
-
Device Owner (DO): Application set as device owner via Device Policy Manager
- Requires device provisioning or ADB setup
- Full device management capabilities
- Cannot be disabled by users
-
Profile Owner (PO): Application set as profile owner for work profiles
- Manages work profile on personally owned devices
- Suitable for BYOD scenarios
Important: Knox license activation will fail with error code 301 (ERROR_INTERNAL) if the application does not have proper Device Administrator privileges. See the troubleshooting section below for implementation guidance.
Note: The library does not include dependencies on specific device detection libraries. Applications can implement their own device detection logic through the LicenseSelectionStrategy interface.
The library provides comprehensive error mapping for Knox SDK error codes:
ERROR_NONE: No errorERROR_INVALID_LICENSE: Invalid license keyERROR_LICENSE_TERMINATED: License has been terminatedERROR_LICENSE_EXPIRED: License has expiredERROR_NETWORK_DISCONNECTED: Network disconnected- And many more...
All errors are returned as LicenseResult.Error with descriptive messages and original error codes.
Knox license keys are sensitive credentials that provide access to enterprise features. Proper security measures are essential for production deployments.
-
local.properties: Store environment-specific keys (never committed to VCS)
# Default/fallback license knox.license=KNOX000000000000000000000000000000000000000000000000000000000000 # Environment-specific licenses knox.license.development=KNOX111111111111111111111111111111111111111111111111111111111111 knox.license.production=KNOX222222222222222222222222222222222222222222222222222222222222 knox.license.staging=KNOX333333333333333333333333333333333333333333333333333333333333
-
Environment Variables: Alternative for CI/CD pipelines
export KNOX_LICENSE_KEY="KNOX000000000000000000000000000000000000000000000000000000000000"
-
Encrypted SharedPreferences (Recommended for most apps):
private fun getEncryptedPreferences(context: Context): SharedPreferences { val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() return EncryptedSharedPreferences.create( context, "knox_secure_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) }
-
Android Keystore (Maximum Security):
private fun storeInKeystore(alias: String, data: ByteArray) { val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeystore") val keyGenParameterSpec = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setUserAuthenticationRequired(false) .build() keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey() // Encrypt and store license key }
-
Server-side Key Management (Enterprise):
- License keys fetched from secure backend
- Short-lived tokens with refresh mechanism
- Network security with certificate pinning
- Device authentication and authorization
-
Code Obfuscation: Enable ProGuard/R8 in production builds
android { buildTypes { release { isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) } } } -
Certificate Pinning: For server-side key retrieval
val certificatePinner = CertificatePinner.Builder() .add("your-api-domain.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") .build()
-
Root Detection: Knox licenses may fail on rooted devices
private fun isDeviceSecure(): Boolean { // Implement root detection logic // Check for common root indicators return !isRooted() }
-
License Validation: Regular validation in production
// Periodically validate license status lifecycleScope.launch { val licenseInfo = knoxLicenseHandler.getLicenseInfo() if (licenseInfo is LicenseInfo.Valid) { // Check expiration, terms, etc. } }
When implementing server-side license management:
- TLS 1.3: Use latest TLS version for key exchange
- Certificate Pinning: Pin server certificates to prevent MITM
- Request Signing: Sign API requests with device-specific keys
- Token-based Auth: Use short-lived tokens instead of static keys
Device Administrator privileges are powerful and require careful handling:
- Principle of Least Privilege: Only request necessary DA permissions
- User Consent: Clearly explain why DA privileges are needed
- Secure Storage: Protect any DA-related configuration data
- Audit Logging: Log DA privilege usage for security monitoring
For Knox-enabled devices with container support:
- Container Isolation: License applies to specific container
- Cross-container Restrictions: Licenses don't transfer between containers
- Data Leakage: Prevent license data from crossing container boundaries
Critical Security Notice: Never commit license keys to version control, disable debugging in production builds, and regularly rotate license keys in enterprise environments.
Knox license activation involves network communication and cryptographic operations. Consider these performance implications:
-
Asynchronous Initialization: Always initialize licenses on background threads
class MyApplication : Application() { override fun onCreate() { super.onCreate() // Launch on IO thread to avoid blocking main thread lifecycleScope.launch(Dispatchers.IO) { KnoxStartupManager.initializeKnoxLicensing(this@MyApplication) } } }
-
Startup Time Impact: License activation can take 2-5 seconds
- First activation: 3-5 seconds (network validation)
- Subsequent startups: 1-2 seconds (cached validation)
- Offline mode: < 500ms (local validation only)
-
Progressive Loading: Don't block UI on license status
@Composable fun MyApp() { var licenseReady by remember { mutableStateOf(false) } LaunchedEffect(Unit) { licenseReady = KnoxStartupManager.isKnoxLicenseReady() } if (licenseReady) { FullAppContent() } else { LoadingScreen() } }
- Memory Footprint: Knox SDK adds ~2-5MB to app memory
- License Caching: Cached license data uses ~100KB-500KB
- Background Services: Knox may spawn background services
-
License Validation: Network calls during activation
- Initial activation: 1-2 network round trips
- Periodic validation: Background network usage
- Offline tolerance: 7-30 days depending on license type
-
Optimization Strategies:
// Cache license status to reduce network calls private var lastValidationTime = 0L private val VALIDATION_INTERVAL = TimeUnit.HOURS.toMillis(24) suspend fun checkLicense(): LicenseInfo { val now = System.currentTimeMillis() if (now - lastValidationTime < VALIDATION_INTERVAL) { return getCachedLicenseInfo() } return knoxLicenseHandler.getLicenseInfo().also { lastValidationTime = now } }
Knox licensing has minimal battery impact when properly implemented:
- Activation: One-time battery cost during initial setup
- Background Validation: Minimal periodic network activity
- Best Practices:
- Batch license operations
- Avoid frequent license status checks
- Use observer patterns instead of polling
-
Thread Safety: Knox SDK operations are not thread-safe
class KnoxLicenseRepository { private val mutex = Mutex() suspend fun activateLicense(): LicenseResult = mutex.withLock { knoxLicenseHandler.activate() } }
-
Background Processing: Always use appropriate dispatchers
// For license operations withContext(Dispatchers.IO) { knoxLicenseHandler.activate() } // For UI updates withContext(Dispatchers.Main) { updateLicenseUI(result) }
-
ProGuard/R8 Optimization: Knox SDK works with code minification
# Keep Knox SDK classes -keep class com.samsung.android.knox.** { *; } -keep class com.sec.enterprise.knox.** { *; } -
App Bundle Optimization: Knox features are device-specific
<!-- Use conditional delivery for Knox features --> <conditional-delivery> <conditions> <device-feature android:name="com.samsung.feature.samsung_experience_mobile" /> </conditions> </conditional-delivery>
-
Cold Start Optimization: Defer non-critical Knox operations
override fun onCreate() { super.onCreate() // Critical UI setup first setContent { MyApp() } // Defer Knox initialization Handler(Looper.getMainLooper()).postDelayed({ initializeKnoxLicensing() }, 1000) }
Track these key performance metrics in production:
- License Activation Time: Time from start to successful activation
- Failure Rate: Percentage of failed activations
- Memory Usage: Monitor Knox-related memory consumption
- Network Usage: Track license-related network calls
// Example metrics collection
class LicenseMetrics {
fun recordActivationTime(durationMs: Long) {
// Send to analytics
}
fun recordActivationFailure(errorCode: Int, reason: String) {
// Track failure patterns
}
}This error typically indicates missing Device Administrator privileges. To resolve:
-
Check if your app is a Device Administrator:
val devicePolicyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager val componentName = ComponentName(this, MyDeviceAdminReceiver::class.java) val isAdmin = devicePolicyManager.isAdminActive(componentName) Log.d("DeviceAdmin", "Is Device Administrator: $isAdmin")
-
Implement Device Administrator registration:
// Create DeviceAdminReceiver class MyDeviceAdminReceiver : DeviceAdminReceiver() // Register in AndroidManifest.xml <receiver android:name=".MyDeviceAdminReceiver" 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" /> </intent-filter> </receiver> // Request Device Administrator privileges val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, componentName) intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "This app requires Device Administrator privileges for Knox license management") startActivityForResult(intent, REQUEST_CODE_ENABLE_ADMIN)
-
Create device_admin.xml:
<?xml version="1.0" encoding="utf-8"?> <device-admin xmlns:android="http://schemas.android.com/apk/res/android"> <uses-policies> <limit-password /> <watch-login /> <reset-password /> <force-lock /> <wipe-data /> </uses-policies> </device-admin>
- 301 (ERROR_INTERNAL): Missing Device Administrator privileges or Knox setup issues
- 102 (ERROR_INVALID_LICENSE): Invalid license key format or expired license
- 103 (ERROR_INVALID_PACKAGE_NAME): License not valid for this application package
- 201 (ERROR_NETWORK_DISCONNECTED): Network connectivity required for license validation
For Device Owner setup via ADB (development/testing):
adb shell dpm set-device-owner com.yourpackage/.MyDeviceAdminReceiverThis library is intended for use with Samsung Knox Enterprise SDK. Ensure you have proper Knox licensing agreements in place.