From fb077d5065629bf40c57909ae296d57bbf3259ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Nov 2023 18:02:02 +0100 Subject: [PATCH 1/4] Implement Mobile Device Manager feature with 3 keys. --- .../fcm/VectorFirebaseMessagingService.kt | 12 +++- vector-app/src/main/AndroidManifest.xml | 4 ++ .../im/vector/app/core/di/SingletonModule.kt | 10 ++++ vector-app/src/main/res/values/strings.xml | 4 ++ .../main/res/xml/vector_app_restrictions.xml | 18 ++++++ .../app/core/platform/VectorBaseActivity.kt | 7 +++ .../vector/app/core/pushers/PushersManager.kt | 8 ++- .../app/core/pushers/UnifiedPushHelper.kt | 20 ++++++- .../homeserver/ServerUrlsRepository.kt | 3 +- .../app/features/mdm/DefaultMdmService.kt | 56 +++++++++++++++++++ .../im/vector/app/features/mdm/MdmService.kt | 39 +++++++++++++ .../onboarding/OnboardingViewModel.kt | 5 +- 12 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 vector-app/src/main/res/xml/vector_app_restrictions.xml create mode 100644 vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt create mode 100644 vector/src/main/java/im/vector/app/features/mdm/MdmService.kt diff --git a/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt index 6ab9b90a84..7a7f14d690 100644 --- a/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt @@ -26,6 +26,8 @@ import im.vector.app.core.pushers.PushParser import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.VectorPushHandler +import im.vector.app.features.mdm.MdmData +import im.vector.app.features.mdm.MdmService import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -46,6 +48,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var pushParser: PushParser @Inject lateinit var vectorPushHandler: VectorPushHandler @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var mdmService: MdmService private val scope = CoroutineScope(SupervisorJob()) @@ -53,6 +56,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { scope.cancel() super.onDestroy() } + override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") fcmHelper.storeFcmToken(token) @@ -62,7 +66,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { unifiedPushHelper.isEmbeddedDistributor() ) { scope.launch { - pushersManager.enqueueRegisterPusher(token, getString(R.string.pusher_http_url)) + pushersManager.enqueueRegisterPusher( + pushKey = token, + gateway = mdmService.getData( + mdmData = MdmData.DefaultPushGatewayUrl, + defaultValue = getString(R.string.pusher_http_url), + ), + ) } } } diff --git a/vector-app/src/main/AndroidManifest.xml b/vector-app/src/main/AndroidManifest.xml index 68325ab512..3668296382 100644 --- a/vector-app/src/main/AndroidManifest.xml +++ b/vector-app/src/main/AndroidManifest.xml @@ -20,6 +20,10 @@ tools:ignore="UnusedAttribute" tools:replace="android:allowBackup"> + + ignored + + Default homeserver URL + Default Push gateway + Permalink base url diff --git a/vector-app/src/main/res/xml/vector_app_restrictions.xml b/vector-app/src/main/res/xml/vector_app_restrictions.xml new file mode 100644 index 0000000000..1932d19e5c --- /dev/null +++ b/vector-app/src/main/res/xml/vector_app_restrictions.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 1e29dfff5e..5bdd960e00 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -76,6 +76,7 @@ import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.consent.ConsentNotGivenHelper +import im.vector.app.features.mdm.MdmService import im.vector.app.features.navigation.Navigator import im.vector.app.features.pin.PinLocker import im.vector.app.features.pin.PinMode @@ -171,6 +172,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var errorFormatter: ErrorFormatter + @Inject lateinit var mdmService: MdmService // For debug only @Inject lateinit var debugReceiver: DebugReceiver @@ -412,6 +414,10 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver rageShake.start() } debugReceiver.register(this) + mdmService.registerListener(this) { + // Just log that a change occurred. + Timber.w("MDM data has been updated") + } } private val postResumeScheduledActions = mutableListOf<() -> Unit>() @@ -442,6 +448,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver rageShake.stop() debugReceiver.unregister(this) + mdmService.unregisterListener(this) } override fun onWindowFocusChanged(hasFocus: Boolean) { diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 402471ecef..1f2441622d 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -22,6 +22,8 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.features.mdm.MdmData +import im.vector.app.features.mdm.MdmService import org.matrix.android.sdk.api.session.pushers.HttpPusher import org.matrix.android.sdk.api.session.pushers.Pusher import java.util.UUID @@ -37,6 +39,7 @@ class PushersManager @Inject constructor( private val stringProvider: StringProvider, private val appNameProvider: AppNameProvider, private val getDeviceInfoUseCase: GetDeviceInfoUseCase, + private val mdmService: MdmService, ) { suspend fun testPush() { val currentSession = activeSessionHolder.getActiveSession() @@ -50,7 +53,10 @@ class PushersManager @Inject constructor( } suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { - return enqueueRegisterPusher(pushKey, stringProvider.getString(R.string.pusher_http_url)) + return enqueueRegisterPusher( + pushKey = pushKey, + gateway = mdmService.getData(MdmData.DefaultPushGatewayUrl, stringProvider.getString(R.string.pusher_http_url)) + ) } suspend fun enqueueRegisterPusher( diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 07052c7146..5b6d91e354 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -24,6 +24,8 @@ import com.squareup.moshi.JsonClass import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.getApplicationLabel +import im.vector.app.features.mdm.MdmData +import im.vector.app.features.mdm.MdmService import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.failure.Failure @@ -40,6 +42,7 @@ class UnifiedPushHelper @Inject constructor( private val stringProvider: StringProvider, private val matrix: Matrix, private val fcmHelper: FcmHelper, + private val mdmService: MdmService, ) { @MainThread @@ -99,7 +102,12 @@ class UnifiedPushHelper @Inject constructor( // register app_id type upfcm on sygnal // the pushkey if FCM key if (UnifiedPush.getDistributor(context) == context.packageName) { - unifiedPushStore.storePushGateway(stringProvider.getString(R.string.pusher_http_url)) + unifiedPushStore.storePushGateway( + gateway = mdmService.getData( + mdmData = MdmData.DefaultPushGatewayUrl, + defaultValue = stringProvider.getString(R.string.pusher_http_url), + ) + ) onDoneRunnable?.run() return } @@ -185,7 +193,13 @@ class UnifiedPushHelper @Inject constructor( } fun getPushGateway(): String? { - return if (isEmbeddedDistributor()) stringProvider.getString(R.string.pusher_http_url) - else unifiedPushStore.getPushGateway() + return if (isEmbeddedDistributor()) { + mdmService.getData( + mdmData = MdmData.DefaultPushGatewayUrl, + defaultValue = stringProvider.getString(R.string.pusher_http_url), + ) + } else { + unifiedPushStore.getPushGateway() + } } } diff --git a/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt index 636c557da9..5b1496028d 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt +++ b/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt @@ -25,6 +25,7 @@ import javax.inject.Inject /** * Object to store and retrieve home and identity server urls. + * Note: this class is not used. */ class ServerUrlsRepository @Inject constructor( @DefaultPreferences @@ -89,5 +90,5 @@ class ServerUrlsRepository @Inject constructor( /** * Return default homeserver url from resources. */ - fun getDefaultHomeServerUrl() = stringProvider.getString(R.string.matrix_org_server_url) + private fun getDefaultHomeServerUrl() = stringProvider.getString(R.string.matrix_org_server_url) } diff --git a/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt b/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt new file mode 100644 index 0000000000..0523a1dd41 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.mdm + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.RestrictionsManager +import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject + +class DefaultMdmService @Inject constructor( + @ApplicationContext applicationContext: Context +) : MdmService { + private val restrictionsManager = applicationContext.getSystemService() + private var onChangedListener: (() -> Unit)? = null + + private val restrictionsReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Timber.w("Restrictions changed") + onChangedListener?.invoke() + } + } + + override fun registerListener(context: Context, onChangedListener: () -> Unit) { + val restrictionsFilter = IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) + this.onChangedListener = onChangedListener + context.registerReceiver(restrictionsReceiver, restrictionsFilter) + } + + override fun unregisterListener(context: Context) { + context.unregisterReceiver(restrictionsReceiver) + this.onChangedListener = null + } + + override fun getData(mdmData: MdmData): String? { + return restrictionsManager?.applicationRestrictions?.getString(mdmData.key) + } +} diff --git a/vector/src/main/java/im/vector/app/features/mdm/MdmService.kt b/vector/src/main/java/im/vector/app/features/mdm/MdmService.kt new file mode 100644 index 0000000000..d601c1658a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/mdm/MdmService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.mdm + +import android.content.Context +import timber.log.Timber + +enum class MdmData(val key: String) { + DefaultHomeserverUrl(key = "im.vector.app.serverConfigDefaultHomeserverUrlString"), + DefaultPushGatewayUrl(key = "im.vector.app.serverConfigSygnalAPIUrlString"), + PermalinkBaseUrl(key = "im.vector.app.clientPermalinkBaseUrl"), +} + +interface MdmService { + fun registerListener(context: Context, onChangedListener: () -> Unit) + fun unregisterListener(context: Context) + fun getData(mdmData: MdmData): String? + fun getData(mdmData: MdmData, defaultValue: String): String { + return getData(mdmData) + ?.also { + Timber.w("Using MDM data for ${mdmData.name}: $it") + } + ?: defaultValue + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index fc6878ffd2..74427736d7 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -46,6 +46,8 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode +import im.vector.app.features.mdm.MdmData +import im.vector.app.features.mdm.MdmService import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult import kotlinx.coroutines.Job @@ -93,6 +95,7 @@ class OnboardingViewModel @AssistedInject constructor( private val registrationActionHandler: RegistrationActionHandler, private val sdkIntProvider: BuildVersionSdkIntProvider, private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase, + mdmService: MdmService, ) : VectorViewModel(initialState) { @AssistedFactory @@ -143,7 +146,7 @@ class OnboardingViewModel @AssistedInject constructor( } private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() - private val defaultHomeserverUrl = matrixOrgUrl + private val defaultHomeserverUrl = mdmService.getData(MdmData.DefaultHomeserverUrl, matrixOrgUrl) private val registrationWizard: RegistrationWizard get() = authenticationService.getRegistrationWizard() From 5987ca9c48ffdd6dc7c4747d411c2b59038196a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Nov 2023 18:24:58 +0100 Subject: [PATCH 2/4] changelog --- changelog.d/8698.feature | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog.d/8698.feature diff --git a/changelog.d/8698.feature b/changelog.d/8698.feature new file mode 100644 index 0000000000..efe11f03cb --- /dev/null +++ b/changelog.d/8698.feature @@ -0,0 +1,5 @@ +Add support for Mobile Device Management. +The keys are: +- default homeserver URL `im.vector.app.serverConfigDefaultHomeserverUrlString` +- push gateway URL `im.vector.app.serverConfigSygnalAPIUrlString` +- permalink base URL `im.vector.app.clientPermalinkBaseUrl` From 882020fdba8c7e10ef24a073f663e0a4783ecfbb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Dec 2023 15:54:17 +0100 Subject: [PATCH 3/4] Fix test compilation issue. --- .../app/core/pushers/PushersManagerTest.kt | 2 ++ .../vector/app/features/mdm/NoOpMdmService.kt | 25 +++++++++++++++++++ .../onboarding/OnboardingViewModelTest.kt | 2 ++ 3 files changed, 29 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/mdm/NoOpMdmService.kt diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt index 4b6063fb93..48850c13c5 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt @@ -17,6 +17,7 @@ package im.vector.app.core.pushers import im.vector.app.R +import im.vector.app.features.mdm.NoOpMdmService import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeAppNameProvider import im.vector.app.test.fakes.FakeGetDeviceInfoUseCase @@ -54,6 +55,7 @@ class PushersManagerTest { stringProvider.instance, appNameProvider, getDeviceInfoUseCase, + NoOpMdmService(), ) @Test diff --git a/vector/src/test/java/im/vector/app/features/mdm/NoOpMdmService.kt b/vector/src/test/java/im/vector/app/features/mdm/NoOpMdmService.kt new file mode 100644 index 0000000000..a8531d8c7f --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/mdm/NoOpMdmService.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.mdm + +import android.content.Context + +class NoOpMdmService : MdmService { + override fun registerListener(context: Context, onChangedListener: () -> Unit) = Unit + override fun unregisterListener(context: Context) = Unit + override fun getData(mdmData: MdmData): String? = null +} diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index a4afab8488..918452b6af 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -26,6 +26,7 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode +import im.vector.app.features.mdm.NoOpMdmService import im.vector.app.features.onboarding.RegistrationStateFixture.aRegistrationState import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult import im.vector.app.test.TestBuildVersionSdkIntProvider @@ -1121,6 +1122,7 @@ class OnboardingViewModelTest { fakeRegistrationActionHandler.instance, TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.O }, fakeConfigureAndStartSessionUseCase, + NoOpMdmService() ).also { viewModel = it initialState = state From 6452b5c2b450da8de1f6ee4717b9940b49616b1e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Dec 2023 17:54:48 +0100 Subject: [PATCH 4/4] Make DefaultMdmService a singleton and use Binds instead of Provides. --- .../main/java/im/vector/app/core/di/SingletonModule.kt | 8 +++----- .../java/im/vector/app/features/mdm/DefaultMdmService.kt | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index 0c250eedb8..9c173268cc 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -111,6 +111,9 @@ import javax.inject.Singleton @Binds abstract fun bindEmojiSpanify(emojiCompatWrapper: EmojiCompatWrapper): EmojiSpanify + @Binds + abstract fun bindMdmService(service: DefaultMdmService): MdmService + @Binds abstract fun bindFontScale(fontScale: FontScalePreferencesImpl): FontScalePreferences @@ -173,11 +176,6 @@ import javax.inject.Singleton return Matrix(context, configuration) } - @Provides - fun providesMdmService(context: Context): MdmService { - return DefaultMdmService(context) - } - @Provides fun providesCurrentSession(activeSessionHolder: ActiveSessionHolder): Session { // TODO handle session injection better diff --git a/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt b/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt index 0523a1dd41..6633f3ce06 100644 --- a/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt +++ b/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt @@ -25,7 +25,9 @@ import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import javax.inject.Inject +import javax.inject.Singleton +@Singleton class DefaultMdmService @Inject constructor( @ApplicationContext applicationContext: Context ) : MdmService {