Commit current workspace state
This commit is contained in:
parent
d2c3c3b534
commit
be4ffb4ede
5 changed files with 141 additions and 2 deletions
|
|
@ -21,7 +21,7 @@ android {
|
|||
versionName "1.25.0"
|
||||
|
||||
buildConfigField 'String', 'ONCALL_DEFAULT_SERVER_URL', '"https://oncall.hugo.dk"'
|
||||
buildConfigField 'String', 'ONCALL_CONFIG_URL', '"https://oncall.hugo.dk/mobile/config"'
|
||||
buildConfigField 'String', 'ONCALL_CONFIG_URL', '"https://ops.centralcloud.com/api/android/config"'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.RestrictionsManager
|
||||
import com.centralcloud.oncall.BuildConfig
|
||||
import com.centralcloud.oncall.db.Repository
|
||||
import com.centralcloud.oncall.sms.SmsRelayPreferences
|
||||
import com.centralcloud.oncall.util.Log
|
||||
|
||||
/**
|
||||
|
|
@ -57,5 +58,16 @@ object OnCallManagedConfig {
|
|||
TAG,
|
||||
"Managed OnCall bootstrap config available: configUrl=$configUrl, environment=$environment, enrollment=${enrollmentToken.isNotEmpty()}",
|
||||
)
|
||||
|
||||
// Store the enrollment token as the device_id used for heartbeats and
|
||||
// remote config auth. Only written once (on first MDM push or reinstall).
|
||||
if (enrollmentToken.isNotEmpty()) {
|
||||
val smsPrefs = SmsRelayPreferences(context)
|
||||
if (smsPrefs.deviceId != enrollmentToken) {
|
||||
smsPrefs.deviceId = enrollmentToken
|
||||
smsPrefs.baseUrl = configUrl.substringBeforeLast("/api").trimEnd('/')
|
||||
Log.i(TAG, "Stored enrollment token as device_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
package com.centralcloud.oncall.sms
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.centralcloud.oncall.BuildConfig
|
||||
import com.centralcloud.oncall.util.HttpUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Fetches the centralcloud-ops remote config for this device and applies
|
||||
* any SMS-relay settings to [SmsRelayPreferences].
|
||||
*
|
||||
* Called once on app start (from MainActivity.onCreate). If the device has
|
||||
* no device_id configured yet the fetch is skipped — nothing to authenticate
|
||||
* with. Config is also re-applied on every successful heartbeat cycle
|
||||
* (DeviceHeartbeatWorker) so it stays fresh without an explicit call here.
|
||||
*
|
||||
* GET {opsUrl}/api/android/config
|
||||
* X-Device-Id: <token>
|
||||
*
|
||||
* Response (relevant fields):
|
||||
* {
|
||||
* "ops_url": "https://ops.centralcloud.com",
|
||||
* "sms_relay": {
|
||||
* "enabled": true,
|
||||
* "whitelist": ["+46701234567", "BANKID"]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
object RemoteConfigFetcher {
|
||||
|
||||
private const val TAG = "RemoteConfigFetcher"
|
||||
|
||||
/**
|
||||
* Fetches remote config and applies it.
|
||||
* Returns true if the fetch succeeded, false on any error.
|
||||
* Must be called from a coroutine — suspends on IO.
|
||||
*/
|
||||
suspend fun fetchAndApply(context: Context): Boolean = withContext(Dispatchers.IO) {
|
||||
val prefs = SmsRelayPreferences(context)
|
||||
val deviceId = prefs.deviceId
|
||||
if (deviceId.isBlank()) {
|
||||
Log.d(TAG, "skip: device_id not set yet")
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
val baseUrl = prefs.baseUrl.ifBlank { BuildConfig.ONCALL_CONFIG_URL.substringBeforeLast("/api") }
|
||||
val url = "${baseUrl.trimEnd('/')}/api/android/config"
|
||||
|
||||
return@withContext try {
|
||||
val client = HttpUtil.defaultClient(context, baseUrl)
|
||||
val request = HttpUtil.requestBuilder(url)
|
||||
.addHeader("X-Device-Id", deviceId)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
Log.w(TAG, "config fetch failed: ${response.code}")
|
||||
return@use false
|
||||
}
|
||||
val body = response.body?.string() ?: return@use false
|
||||
apply(prefs, JSONObject(body))
|
||||
true
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "config fetch error", t)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(prefs: SmsRelayPreferences, json: JSONObject) {
|
||||
// Persist ops_url as the base URL for heartbeats and SMS forwarding.
|
||||
json.optString("ops_url").takeIf { it.isNotBlank() }?.let { opsUrl ->
|
||||
if (prefs.baseUrl != opsUrl) {
|
||||
prefs.baseUrl = opsUrl
|
||||
Log.d(TAG, "updated baseUrl=$opsUrl")
|
||||
}
|
||||
}
|
||||
|
||||
val smsRelay = json.optJSONObject("sms_relay") ?: return
|
||||
val enabled = smsRelay.optBoolean("enabled", false)
|
||||
val whitelist = smsRelay.optJSONArray("whitelist")
|
||||
?.let { arr -> (0 until arr.length()).map { arr.getString(it) }.toSet() }
|
||||
?: emptySet()
|
||||
|
||||
prefs.enabled = enabled
|
||||
prefs.whitelist = whitelist
|
||||
|
||||
Log.d(TAG, "applied sms_relay: enabled=$enabled whitelist_size=${whitelist.size}")
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +60,8 @@ import com.centralcloud.oncall.msg.NotificationDispatcher
|
|||
import com.centralcloud.oncall.msg.Poller
|
||||
import com.centralcloud.oncall.service.SubscriberService
|
||||
import com.centralcloud.oncall.service.SubscriberServiceManager
|
||||
import com.centralcloud.oncall.util.Log
|
||||
import com.centralcloud.oncall.sms.RemoteConfigFetcher
|
||||
import com.centralcloud.oncall.sms.SmsRelayPreferences
|
||||
import com.centralcloud.oncall.util.SUBSCRIPTION_ICONS
|
||||
import com.centralcloud.oncall.util.dangerButton
|
||||
import com.centralcloud.oncall.util.displayName
|
||||
|
|
@ -384,6 +385,13 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
// Permissions
|
||||
maybeRequestNotificationPermission()
|
||||
|
||||
// Fetch remote config (SMS relay whitelist etc.) then ask for SMS
|
||||
// permissions if the server says relay is enabled for this device.
|
||||
lifecycleScope.launch {
|
||||
RemoteConfigFetcher.fetchAndApply(applicationContext)
|
||||
maybeRequestSmsPermissions()
|
||||
}
|
||||
|
||||
// FIXME 2026-05-04: Remove this migration after 1 month
|
||||
migrateSubscriptionIconsFromCache()
|
||||
}
|
||||
|
|
@ -397,6 +405,24 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
}
|
||||
}
|
||||
|
||||
private fun maybeRequestSmsPermissions() {
|
||||
val prefs = SmsRelayPreferences(this)
|
||||
if (!prefs.enabled) return
|
||||
|
||||
val missing = listOf(Manifest.permission.RECEIVE_SMS, Manifest.permission.READ_SMS)
|
||||
.filter { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_DENIED }
|
||||
if (missing.isEmpty()) return
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.sms_relay_permission_dialog_title))
|
||||
.setMessage(getString(R.string.sms_relay_permission_dialog_message))
|
||||
.setPositiveButton(getString(R.string.sms_relay_permission_dialog_grant)) { _, _ ->
|
||||
ActivityCompat.requestPermissions(this, missing.toTypedArray(), REQUEST_CODE_SMS_PERMISSIONS)
|
||||
}
|
||||
.setNegativeButton(getString(R.string.sms_relay_permission_dialog_later), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
showHideNotificationMenuItems()
|
||||
|
|
@ -950,5 +976,6 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
const val POLL_WORKER_INTERVAL_MINUTES = 60L
|
||||
const val DELETE_WORKER_INTERVAL_MINUTES = 8 * 60L
|
||||
const val SERVICE_START_WORKER_INTERVAL_MINUTES = 3 * 60L
|
||||
const val REQUEST_CODE_SMS_PERMISSIONS = 42
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -566,4 +566,10 @@
|
|||
<string name="client_certificate_dialog_error_wrong_password">Wrong password or invalid PKCS#12 file</string>
|
||||
<string name="client_certificate_dialog_error_invalid_p12_password">Invalid password or corrupted PKCS#12 file</string>
|
||||
<string name="client_certificate_dialog_error_invalid_url">Invalid service URL</string>
|
||||
|
||||
<!-- SMS relay permission dialog -->
|
||||
<string name="sms_relay_permission_dialog_title">SMS monitoring enabled</string>
|
||||
<string name="sms_relay_permission_dialog_message">This device is configured to forward SMS messages from monitored numbers to the on-call system. Grant SMS access to enable this feature.</string>
|
||||
<string name="sms_relay_permission_dialog_grant">Grant access</string>
|
||||
<string name="sms_relay_permission_dialog_later">Later</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue