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"
|
versionName "1.25.0"
|
||||||
|
|
||||||
buildConfigField 'String', 'ONCALL_DEFAULT_SERVER_URL', '"https://oncall.hugo.dk"'
|
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"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import android.content.RestrictionsManager
|
import android.content.RestrictionsManager
|
||||||
import com.centralcloud.oncall.BuildConfig
|
import com.centralcloud.oncall.BuildConfig
|
||||||
import com.centralcloud.oncall.db.Repository
|
import com.centralcloud.oncall.db.Repository
|
||||||
|
import com.centralcloud.oncall.sms.SmsRelayPreferences
|
||||||
import com.centralcloud.oncall.util.Log
|
import com.centralcloud.oncall.util.Log
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,5 +58,16 @@ object OnCallManagedConfig {
|
||||||
TAG,
|
TAG,
|
||||||
"Managed OnCall bootstrap config available: configUrl=$configUrl, environment=$environment, enrollment=${enrollmentToken.isNotEmpty()}",
|
"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.msg.Poller
|
||||||
import com.centralcloud.oncall.service.SubscriberService
|
import com.centralcloud.oncall.service.SubscriberService
|
||||||
import com.centralcloud.oncall.service.SubscriberServiceManager
|
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.SUBSCRIPTION_ICONS
|
||||||
import com.centralcloud.oncall.util.dangerButton
|
import com.centralcloud.oncall.util.dangerButton
|
||||||
import com.centralcloud.oncall.util.displayName
|
import com.centralcloud.oncall.util.displayName
|
||||||
|
|
@ -384,6 +385,13 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
||||||
// Permissions
|
// Permissions
|
||||||
maybeRequestNotificationPermission()
|
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
|
// FIXME 2026-05-04: Remove this migration after 1 month
|
||||||
migrateSubscriptionIconsFromCache()
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
showHideNotificationMenuItems()
|
showHideNotificationMenuItems()
|
||||||
|
|
@ -950,5 +976,6 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
||||||
const val POLL_WORKER_INTERVAL_MINUTES = 60L
|
const val POLL_WORKER_INTERVAL_MINUTES = 60L
|
||||||
const val DELETE_WORKER_INTERVAL_MINUTES = 8 * 60L
|
const val DELETE_WORKER_INTERVAL_MINUTES = 8 * 60L
|
||||||
const val SERVICE_START_WORKER_INTERVAL_MINUTES = 3 * 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_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_p12_password">Invalid password or corrupted PKCS#12 file</string>
|
||||||
<string name="client_certificate_dialog_error_invalid_url">Invalid service URL</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>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue