en
Developers
Managed Config (MDM)

Managed Configuration (MDM)

NTP DIG PING MORE supports Android Managed Configurations (formerly App Restrictions), allowing enterprise Mobile Device Management platforms to remotely pre-configure the application without requiring users to manually enter network parameters.


Architecture overview

MDM console
    │  pushes Bundle via RestrictionsManager

ManagedConfigRepository  ←─ BroadcastReceiver (live updates)
    │  StateFlow<ManagedConfig>

ViewModels (NTP, DIG, Ping, PortScanner, HttpsCert, Settings, BulkActions)
    │  apply as overridable defaults

UI state  (user can still edit all fields)

Key files

FilePurpose
app/src/main/res/xml/app_restrictions.xmlSchema exposed to MDM consoles
settings/ManagedConfigRepository.ktReads RestrictionsManager, exposes StateFlow<ManagedConfig>
settings/ManagedConfig.ktTyped data class (14 fields)
AndroidManifest.xmlandroid.content.APP_RESTRICTIONS meta-data registration

ManagedConfigRepository

// Instantiation (already wired in each ViewModel factory)
val repo = ManagedConfigRepository(context)
 
// Consume in a ViewModel coroutine
viewModelScope.launch {
    repo.configFlow.collect { config ->
        config.ntpServer?.let { _uiState.update { s -> s.copy(serverAddress = it) } }
    }
}
 
// Check managed status (e.g. for the Device Info badge)
val isManaged: Boolean = repo.isAppManaged
 
// Clean up (called automatically from ViewModel.onCleared)
repo.unregister()

Live updates

ManagedConfigRepository registers a BroadcastReceiver for Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED.
When the MDM admin changes a restriction the configFlow emits a new value without restarting the app.

Non-managed devices

RestrictionsManager.getApplicationRestrictions() returns an empty Bundle on non-managed devices. All ManagedConfig fields default to null / false, so the app behaves identically to before — no code path is affected.


Restriction keys

Network tools (all string type)

KeyViewModel fieldDefault
ntp_default_serverNtpUiState.serverAddresspool.ntp.org
ntp_default_portNtpUiState.port123
dig_default_serverDigUiState.dnsServer""
dig_default_fqdnDigUiState.fqdn""
ping_default_hostPingUiState.host""
port_scanner_default_hostPortScannerUiState.host""
https_cert_default_hostHttpsCertViewModel._hostgoogle.com
https_cert_default_portHttpsCertViewModel._port443

Proxy PAC (Settings screen)

KeyTypeDefaultViewModel field
proxy_enabledboolfalseSettingsUiState.proxyEnabled
proxy_pac_urlstring""SettingsUiState.proxyPacUrl
proxy_logging_enabledboolfalseSettingsUiState.proxyLoggingEnabled

Bulk Actions (zero-touch provisioning)

KeyTypeDefaultBehaviour
bulk_actions_jsonstring""Inline JSON payload — parsed directly, no file transfer needed
bulk_actions_urlstring""URL fetched at startup if bulk_actions_json is absent
bulk_actions_auto_runboolfalseExecutes the loaded config on launch without user interaction

bulk_actions_json takes precedence over bulk_actions_url when both are set.


Overridable defaults pattern

Every ViewModel follows the same guard pattern to avoid overwriting user edits mid-session:

managedConfigRepository?.let { repo ->
    viewModelScope.launch {
        repo.configFlow.collect { config ->
            val current = _uiState.value
            val newServer = config.ntpServer ?: current.serverAddress  // keep user value if MDM absent
            if (newServer != current.serverAddress) {
                _uiState.value = current.copy(serverAddress = newServer)
            }
        }
    }
}

The user can always change any field after it has been pre-filled — MDM provides an initial value, not a lock.


Unit testing

ManagedConfigRepository accepts an android.os.Bundle via RestrictionsManager. In unit tests, pass a null (or a mocked repository) to isolate ViewModel logic:

val vm = SimpleNtpViewModel(
    historyStore = mockHistoryStore,
    settingsRepository = mockSettingsRepo,
    managedConfigRepository = null,   // no MDM in tests by default
)

To test MDM defaults:

val fakeRepo = mockk<ManagedConfigRepository> {
    every { configFlow } returns flowOf(ManagedConfig(ntpServer = "time.corp.example.com"))
}
val vm = SimpleNtpViewModel(
    historyStore = mockHistoryStore,
    settingsRepository = mockSettingsRepo,
    managedConfigRepository = fakeRepo,
)
advanceUntilIdle()
assertEquals("time.corp.example.com", vm.uiState.value.serverAddress)

MIT 2026 © Nextra.