Merge pull request #7159 from vector-im/feature/bma/fix_new_lint_warning
Fix lint warning
This commit is contained in:
		
						commit
						60bfd0dd42
					
				
							
								
								
									
										1
									
								
								changelog.d/7159.misc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelog.d/7159.misc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Fix lint warning, and cleanup the code | ||||
| @ -14,6 +14,7 @@ | ||||
|         android:id="@+id/menuDebug2" | ||||
|         android:icon="@drawable/ic_debug_icon" | ||||
|         android:title="Send" | ||||
|         app:showAsAction="always" /> | ||||
|         app:showAsAction="always" | ||||
|         tools:ignore="AlwaysShowAction" /> | ||||
| 
 | ||||
| </menu> | ||||
| </menu> | ||||
|  | ||||
| @ -131,11 +131,10 @@ class SecretStoringUtils @Inject constructor( | ||||
|      * | ||||
|      * The secret is encrypted using the following method: AES/GCM/NoPadding | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     @Throws(Exception::class) | ||||
|     fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray { | ||||
|         return when { | ||||
|             buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias) | ||||
|             buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> encryptBytesM(secret, keyAlias) | ||||
|             else -> encryptBytes(secret, keyAlias) | ||||
|         } | ||||
|     } | ||||
| @ -156,10 +155,9 @@ class SecretStoringUtils @Inject constructor( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("NewApi") | ||||
|     fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { | ||||
|         when { | ||||
|             buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) | ||||
|             buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> saveSecureObjectM(keyAlias, output, any) | ||||
|             else -> saveSecureObject(keyAlias, output, any) | ||||
|         } | ||||
|     } | ||||
| @ -189,7 +187,6 @@ class SecretStoringUtils @Inject constructor( | ||||
|         return cipher | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("NewApi") | ||||
|     @RequiresApi(Build.VERSION_CODES.M) | ||||
|     private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey { | ||||
|         val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) | ||||
|  | ||||
| @ -16,6 +16,8 @@ | ||||
| 
 | ||||
| package org.matrix.android.sdk.api.util | ||||
| 
 | ||||
| import androidx.annotation.ChecksSdkIntAtLeast | ||||
| 
 | ||||
| interface BuildVersionSdkIntProvider { | ||||
|     /** | ||||
|      * Return the current version of the Android SDK. | ||||
| @ -26,9 +28,13 @@ interface BuildVersionSdkIntProvider { | ||||
|      * Checks the if the current OS version is equal or greater than [version]. | ||||
|      * @return A `non-null` result if true, `null` otherwise. | ||||
|      */ | ||||
|     @ChecksSdkIntAtLeast(parameter = 0, lambda = 1) | ||||
|     fun <T> whenAtLeast(version: Int, result: () -> T): T? { | ||||
|         return if (get() >= version) { | ||||
|             result() | ||||
|         } else null | ||||
|     } | ||||
| 
 | ||||
|     @ChecksSdkIntAtLeast(parameter = 0) | ||||
|     fun isAtLeast(version: Int) = get() >= version | ||||
| } | ||||
|  | ||||
| @ -19,6 +19,9 @@ | ||||
|     <issue id="IconExpectedSize" severity="error" /> | ||||
|     <issue id="LocaleFolder" severity="error" /> | ||||
| 
 | ||||
|     <!-- AlwaysShowAction is considered as an error to force ignoring the issue when detected --> | ||||
|     <issue id="AlwaysShowAction" severity="error" /> | ||||
| 
 | ||||
|     <issue id="TooManyViews" severity="warning"> | ||||
|         <!-- Ignore TooManyViews in debug build type --> | ||||
|         <ignore path="**/src/debug/**" /> | ||||
| @ -77,6 +80,7 @@ | ||||
|     <issue id="KotlinPropertyAccess" severity="error" /> | ||||
|     <issue id="DefaultLocale" severity="error" /> | ||||
|     <issue id="CheckResult" severity="error" /> | ||||
|     <issue id="StaticFieldLeak" severity="error" /> | ||||
| 
 | ||||
|     <issue id="InvalidPackage"> | ||||
|         <!-- Ignore error from HtmlCompressor lib --> | ||||
| @ -105,6 +109,9 @@ | ||||
|     <issue id="TypographyDashes" severity="error" /> | ||||
|     <issue id="PluralsCandidate" severity="error" /> | ||||
| 
 | ||||
|     <!-- Notification --> | ||||
|     <issue id="LaunchActivityFromNotification" severity="error" /> | ||||
| 
 | ||||
|     <!-- DI --> | ||||
|     <issue id="JvmStaticProvidesInObjectDetector" severity="error" /> | ||||
| </lint> | ||||
|  | ||||
| @ -23,7 +23,7 @@ import android.content.IntentFilter | ||||
| import android.content.SharedPreferences | ||||
| import androidx.core.content.edit | ||||
| import im.vector.app.core.debug.DebugReceiver | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.core.utils.lsFiles | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| @ -31,7 +31,10 @@ import javax.inject.Inject | ||||
| /** | ||||
|  * Receiver to handle some command from ADB | ||||
|  */ | ||||
| class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugReceiver { | ||||
| class VectorDebugReceiver @Inject constructor( | ||||
|         @DefaultPreferences | ||||
|         private val sharedPreferences: SharedPreferences, | ||||
| ) : BroadcastReceiver(), DebugReceiver { | ||||
| 
 | ||||
|     override fun register(context: Context) { | ||||
|         context.registerReceiver(this, getIntentFilter(context)) | ||||
| @ -47,14 +50,14 @@ class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugRece | ||||
|         intent.action?.let { | ||||
|             when { | ||||
|                 it.endsWith(DEBUG_ACTION_DUMP_FILESYSTEM) -> lsFiles(context) | ||||
|                 it.endsWith(DEBUG_ACTION_DUMP_PREFERENCES) -> dumpPreferences(context) | ||||
|                 it.endsWith(DEBUG_ACTION_ALTER_SCALAR_TOKEN) -> alterScalarToken(context) | ||||
|                 it.endsWith(DEBUG_ACTION_DUMP_PREFERENCES) -> dumpPreferences() | ||||
|                 it.endsWith(DEBUG_ACTION_ALTER_SCALAR_TOKEN) -> alterScalarToken() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun dumpPreferences(context: Context) { | ||||
|         logPrefs("DefaultSharedPreferences", DefaultSharedPreferences.getInstance(context)) | ||||
|     private fun dumpPreferences() { | ||||
|         logPrefs("DefaultSharedPreferences", sharedPreferences) | ||||
|     } | ||||
| 
 | ||||
|     private fun logPrefs(name: String, sharedPreferences: SharedPreferences?) { | ||||
| @ -67,8 +70,8 @@ class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugRece | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun alterScalarToken(context: Context) { | ||||
|         DefaultSharedPreferences.getInstance(context).edit { | ||||
|     private fun alterScalarToken() { | ||||
|         sharedPreferences.edit { | ||||
|             // putString("SCALAR_TOKEN_PREFERENCE_KEY" + Matrix.getInstance(context).defaultSession.myUserId, "bad_token") | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -17,6 +17,7 @@ package im.vector.app.push.fcm | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import android.widget.Toast | ||||
| import androidx.core.content.edit | ||||
| import com.google.android.gms.common.ConnectionResult | ||||
| @ -24,7 +25,7 @@ import com.google.android.gms.common.GoogleApiAvailability | ||||
| import com.google.firebase.messaging.FirebaseMessaging | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.di.ActiveSessionHolder | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.core.pushers.FcmHelper | ||||
| import im.vector.app.core.pushers.PushersManager | ||||
| import timber.log.Timber | ||||
| @ -35,14 +36,13 @@ import javax.inject.Inject | ||||
|  * It has an alter ego in the fdroid variant. | ||||
|  */ | ||||
| class GoogleFcmHelper @Inject constructor( | ||||
|         context: Context, | ||||
|         @DefaultPreferences | ||||
|         private val sharedPrefs: SharedPreferences, | ||||
| ) : FcmHelper { | ||||
|     companion object { | ||||
|         private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" | ||||
|     } | ||||
| 
 | ||||
|     private val sharedPrefs = DefaultSharedPreferences.getInstance(context) | ||||
| 
 | ||||
|     override fun isFirebaseAvailable(): Boolean = true | ||||
| 
 | ||||
|     override fun getFcmToken(): String? { | ||||
|  | ||||
| @ -53,7 +53,7 @@ import im.vector.app.core.resources.BuildMeta | ||||
| import im.vector.app.features.analytics.VectorAnalytics | ||||
| import im.vector.app.features.call.webrtc.WebRtcCallManager | ||||
| import im.vector.app.features.configuration.VectorConfiguration | ||||
| import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog | ||||
| import im.vector.app.features.disclaimer.DisclaimerDialog | ||||
| import im.vector.app.features.invite.InvitesAcceptor | ||||
| import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks | ||||
| import im.vector.app.features.notifications.NotificationDrawerManager | ||||
| @ -109,6 +109,8 @@ class VectorApplication : | ||||
|     @Inject lateinit var fcmHelper: FcmHelper | ||||
|     @Inject lateinit var buildMeta: BuildMeta | ||||
|     @Inject lateinit var leakDetector: LeakDetector | ||||
|     @Inject lateinit var vectorLocale: VectorLocale | ||||
|     @Inject lateinit var disclaimerDialog: DisclaimerDialog | ||||
| 
 | ||||
|     // font thread handler | ||||
|     private var fontThreadHandler: Handler? = null | ||||
| @ -159,7 +161,7 @@ class VectorApplication : | ||||
|                 R.array.com_google_android_gms_fonts_certs | ||||
|         ) | ||||
|         FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) | ||||
|         VectorLocale.init(this, buildMeta) | ||||
|         vectorLocale.init() | ||||
|         ThemeUtils.init(this) | ||||
|         vectorConfiguration.applyToApplicationContext() | ||||
| 
 | ||||
| @ -171,7 +173,7 @@ class VectorApplication : | ||||
|         val sessionImported = legacySessionImporter.process() | ||||
|         if (!sessionImported) { | ||||
|             // Do not display the name change popup | ||||
|             doNotShowDisclaimerDialog(this) | ||||
|             disclaimerDialog.doNotShowDisclaimerDialog() | ||||
|         } | ||||
| 
 | ||||
|         ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { | ||||
|  | ||||
| @ -1,46 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022 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 | ||||
| 
 | ||||
| import android.os.Build | ||||
| import java.lang.reflect.Field | ||||
| 
 | ||||
| /** | ||||
|  * Used to override [Build.VERSION.SDK_INT]. Ideally an interface should be used instead, but that approach forces us to either add suppress lint annotations | ||||
|  * and potentially miss an API version issue or write a custom lint rule, which seems like an overkill. | ||||
|  */ | ||||
| object AndroidVersionTestOverrider { | ||||
| 
 | ||||
|     private var initialValue: Int? = null | ||||
| 
 | ||||
|     fun override(newVersion: Int) { | ||||
|         if (initialValue == null) { | ||||
|             initialValue = Build.VERSION.SDK_INT | ||||
|         } | ||||
|         val field = Build.VERSION::class.java.getField("SDK_INT") | ||||
|         setStaticField(field, newVersion) | ||||
|     } | ||||
| 
 | ||||
|     fun restore() { | ||||
|         initialValue?.let { override(it) } | ||||
|     } | ||||
| 
 | ||||
|     private fun setStaticField(field: Field, value: Any) { | ||||
|         field.isAccessible = true | ||||
|         field.set(null, value) | ||||
|     } | ||||
| } | ||||
| @ -18,8 +18,6 @@ package im.vector.app | ||||
| 
 | ||||
| import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider | ||||
| 
 | ||||
| class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider { | ||||
|     var value: Int = 0 | ||||
| 
 | ||||
| class TestBuildVersionSdkIntProvider(var value: Int = 0) : BuildVersionSdkIntProvider { | ||||
|     override fun get() = value | ||||
| } | ||||
|  | ||||
| @ -25,6 +25,7 @@ import android.security.keystore.KeyProperties | ||||
| import android.util.Base64 | ||||
| import androidx.preference.PreferenceManager | ||||
| import androidx.test.platform.app.InstrumentationRegistry | ||||
| import im.vector.app.TestBuildVersionSdkIntProvider | ||||
| import im.vector.app.features.pin.PinCodeStore | ||||
| import im.vector.app.features.pin.SharedPrefPinCodeStore | ||||
| import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.ANDROID_KEY_STORE | ||||
| @ -32,7 +33,6 @@ import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LE | ||||
| import io.mockk.coEvery | ||||
| import io.mockk.coVerify | ||||
| import io.mockk.every | ||||
| import io.mockk.mockk | ||||
| import io.mockk.spyk | ||||
| import io.mockk.verify | ||||
| import kotlinx.coroutines.runBlocking | ||||
| @ -42,7 +42,6 @@ import org.amshove.kluent.shouldBeEqualTo | ||||
| import org.junit.After | ||||
| import org.junit.Test | ||||
| import org.matrix.android.sdk.api.securestorage.SecretStoringUtils | ||||
| import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider | ||||
| import java.math.BigInteger | ||||
| import java.security.KeyFactory | ||||
| import java.security.KeyPairGenerator | ||||
| @ -66,9 +65,7 @@ class LegacyPinCodeMigratorTests { | ||||
|             SharedPrefPinCodeStore(PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().context)) | ||||
|     ) | ||||
|     private val keyStore: KeyStore = spyk(KeyStore.getInstance(ANDROID_KEY_STORE)).also { it.load(null) } | ||||
|     private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider = mockk { | ||||
|         every { get() } returns Build.VERSION_CODES.M | ||||
|     } | ||||
|     private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider(Build.VERSION_CODES.M) | ||||
|     private val secretStoringUtils: SecretStoringUtils = spyk( | ||||
|             SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider) | ||||
|     ) | ||||
| @ -125,26 +122,18 @@ class LegacyPinCodeMigratorTests { | ||||
| 
 | ||||
|     @Test | ||||
|     fun migratePinCodeM() = runTest { | ||||
|         val pinCode = "1234" | ||||
|         saveLegacyPinCode(pinCode) | ||||
| 
 | ||||
|         legacyPinCodeMigrator.migrate() | ||||
| 
 | ||||
|         coVerify { legacyPinCodeMigrator.getDecryptedPinCode() } | ||||
|         verify { secretStoringUtils.securelyStoreBytes(any(), any()) } | ||||
|         coVerify { pinCodeStore.savePinCode(any()) } | ||||
|         verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } | ||||
| 
 | ||||
|         val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias)) | ||||
|         decodedPinCode shouldBeEqualTo pinCode | ||||
|         keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false | ||||
|         keyStore.containsAlias(alias) shouldBe true | ||||
|         buildVersionSdkIntProvider.value = Build.VERSION_CODES.M | ||||
|         migratePinCode() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun migratePinCodeL() = runTest { | ||||
|         buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP | ||||
|         migratePinCode() | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun migratePinCode() { | ||||
|         val pinCode = "1234" | ||||
|         every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP | ||||
|         saveLegacyPinCode(pinCode) | ||||
| 
 | ||||
|         legacyPinCodeMigrator.migrate() | ||||
| @ -163,7 +152,7 @@ class LegacyPinCodeMigratorTests { | ||||
|     private fun generateLegacyKey() { | ||||
|         if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return | ||||
| 
 | ||||
|         if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) { | ||||
|         if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) { | ||||
|             generateLegacyKeyM() | ||||
|         } else { | ||||
|             generateLegacyKeyL() | ||||
| @ -206,7 +195,7 @@ class LegacyPinCodeMigratorTests { | ||||
|         generateLegacyKey() | ||||
|         val publicKey = keyStore.getCertificate(LEGACY_PIN_CODE_KEY_ALIAS).publicKey | ||||
|         val cipher = getLegacyCipher() | ||||
|         if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) { | ||||
|         if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) { | ||||
|             val unrestrictedKey = KeyFactory.getInstance(publicKey.algorithm).generatePublic(X509EncodedKeySpec(publicKey.encoded)) | ||||
|             val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT) | ||||
|             cipher.init(Cipher.ENCRYPT_MODE, unrestrictedKey, spec) | ||||
| @ -219,14 +208,15 @@ class LegacyPinCodeMigratorTests { | ||||
|     } | ||||
| 
 | ||||
|     private fun getLegacyCipher(): Cipher { | ||||
|         return when (buildVersionSdkIntProvider.get()) { | ||||
|             Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1 -> getCipherL() | ||||
|             else -> getCipherM() | ||||
|         return if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) { | ||||
|             getCipherM() | ||||
|         } else { | ||||
|             getCipherL() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getCipherL(): Cipher { | ||||
|         val provider = if (buildVersionSdkIntProvider.get() < Build.VERSION_CODES.M) "AndroidOpenSSL" else "AndroidKeyStoreBCWorkaround" | ||||
|         val provider = if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) "AndroidKeyStoreBCWorkaround" else "AndroidOpenSSL" | ||||
|         val transformation = "RSA/ECB/PKCS1Padding" | ||||
|         return Cipher.getInstance(transformation, provider) | ||||
|     } | ||||
|  | ||||
| @ -18,41 +18,36 @@ package im.vector.app.features.voice | ||||
| 
 | ||||
| import android.os.Build | ||||
| import androidx.test.platform.app.InstrumentationRegistry | ||||
| import im.vector.app.AndroidVersionTestOverrider | ||||
| import im.vector.app.TestBuildVersionSdkIntProvider | ||||
| import im.vector.app.features.DefaultVectorFeatures | ||||
| import io.mockk.every | ||||
| import io.mockk.spyk | ||||
| import org.amshove.kluent.shouldBeInstanceOf | ||||
| import org.junit.After | ||||
| import org.junit.Test | ||||
| 
 | ||||
| class VoiceRecorderProviderTests { | ||||
| 
 | ||||
|     private val context = InstrumentationRegistry.getInstrumentation().targetContext | ||||
|     private val provider = spyk(VoiceRecorderProvider(context, DefaultVectorFeatures())) | ||||
| 
 | ||||
|     @After | ||||
|     fun tearDown() { | ||||
|         AndroidVersionTestOverrider.restore() | ||||
|     } | ||||
|     private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() | ||||
|     private val provider = spyk(VoiceRecorderProvider(context, DefaultVectorFeatures(), buildVersionSdkIntProvider)) | ||||
| 
 | ||||
|     @Test | ||||
|     fun provideVoiceRecorderOnAndroidQAndCodecReturnsQRecorder() { | ||||
|         AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q) | ||||
|         buildVersionSdkIntProvider.value = Build.VERSION_CODES.Q | ||||
|         every { provider.hasOpusEncoder() } returns true | ||||
|         provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderQ::class) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun provideVoiceRecorderOnAndroidQButNoCodecReturnsLRecorder() { | ||||
|         AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q) | ||||
|         buildVersionSdkIntProvider.value = Build.VERSION_CODES.Q | ||||
|         every { provider.hasOpusEncoder() } returns false | ||||
|         provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun provideVoiceRecorderOnOlderAndroidVersionReturnsLRecorder() { | ||||
|         AndroidVersionTestOverrider.override(Build.VERSION_CODES.LOLLIPOP) | ||||
|         buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP | ||||
|         provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| package im.vector.app.core.extensions | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.graphics.drawable.Drawable | ||||
| import android.net.ConnectivityManager | ||||
| @ -91,10 +90,9 @@ fun Context.safeOpenOutputStream(uri: Uri): OutputStream? { | ||||
|  * | ||||
|  * @return true if no active connection is found | ||||
|  */ | ||||
| @SuppressLint("NewApi") // false positive | ||||
| fun Context.inferNoConnectivity(sdkIntProvider: BuildVersionSdkIntProvider): Boolean { | ||||
|     val connectivityManager = getSystemService<ConnectivityManager>()!! | ||||
|     return if (sdkIntProvider.get() > Build.VERSION_CODES.M) { | ||||
|     return if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) { | ||||
|         val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) | ||||
|         when { | ||||
|             networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false | ||||
|  | ||||
| @ -84,6 +84,7 @@ import im.vector.app.features.rageshake.RageShake | ||||
| import im.vector.app.features.session.SessionListener | ||||
| import im.vector.app.features.settings.FontScalePreferences | ||||
| import im.vector.app.features.settings.FontScalePreferencesImpl | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import im.vector.app.features.settings.VectorPreferences | ||||
| import im.vector.app.features.themes.ActivityOtherThemes | ||||
| import im.vector.app.features.themes.ThemeUtils | ||||
| @ -155,6 +156,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver | ||||
|     @Inject lateinit var rageShake: RageShake | ||||
|     @Inject lateinit var buildMeta: BuildMeta | ||||
|     @Inject lateinit var fontScalePreferences: FontScalePreferences | ||||
|     @Inject lateinit var vectorLocale: VectorLocaleProvider | ||||
| 
 | ||||
|     // For debug only | ||||
|     @Inject lateinit var debugReceiver: DebugReceiver | ||||
| @ -176,8 +178,10 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver | ||||
|     private val restorables = ArrayList<Restorable>() | ||||
| 
 | ||||
|     override fun attachBaseContext(base: Context) { | ||||
|         val fontScalePreferences = FontScalePreferencesImpl(PreferenceManager.getDefaultSharedPreferences(base), AndroidSystemSettingsProvider(base)) | ||||
|         val vectorConfiguration = VectorConfiguration(this, fontScalePreferences) | ||||
|         val preferences = PreferenceManager.getDefaultSharedPreferences(base) | ||||
|         val fontScalePreferences = FontScalePreferencesImpl(preferences, AndroidSystemSettingsProvider(base)) | ||||
|         val vectorLocaleProvider = VectorLocaleProvider(preferences) | ||||
|         val vectorConfiguration = VectorConfiguration(this, fontScalePreferences, vectorLocaleProvider) | ||||
|         super.attachBaseContext(vectorConfiguration.getLocalisedContext(base)) | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -17,16 +17,17 @@ | ||||
| package im.vector.app.core.pushers | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import androidx.core.content.edit | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class UnifiedPushStore @Inject constructor( | ||||
|         val context: Context, | ||||
|         val fcmHelper: FcmHelper | ||||
|         val fcmHelper: FcmHelper, | ||||
|         @DefaultPreferences | ||||
|         private val defaultPrefs: SharedPreferences, | ||||
| ) { | ||||
|     private val defaultPrefs = DefaultSharedPreferences.getInstance(context) | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieves the UnifiedPush Endpoint. | ||||
|      * | ||||
|  | ||||
| @ -20,11 +20,10 @@ import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.view.View | ||||
| import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import androidx.core.content.edit | ||||
| import androidx.core.view.isVisible | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.databinding.ViewKeysBackupBannerBinding | ||||
| import im.vector.app.features.workers.signout.BannerState | ||||
| import timber.log.Timber | ||||
| 
 | ||||
| /** | ||||
| @ -38,16 +37,12 @@ class KeysBackupBanner @JvmOverloads constructor( | ||||
| ) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener { | ||||
| 
 | ||||
|     var delegate: Delegate? = null | ||||
|     private var state: State = State.Initial | ||||
|     private var state: BannerState = BannerState.Initial | ||||
| 
 | ||||
|     private lateinit var views: ViewKeysBackupBannerBinding | ||||
| 
 | ||||
|     init { | ||||
|         setupView() | ||||
|         DefaultSharedPreferences.getInstance(context).edit { | ||||
|             putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) | ||||
|             putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -56,7 +51,7 @@ class KeysBackupBanner @JvmOverloads constructor( | ||||
|      * @param newState the newState representing the view | ||||
|      * @param force true to force the rendering of the view | ||||
|      */ | ||||
|     fun render(newState: State, force: Boolean = false) { | ||||
|     fun render(newState: BannerState, force: Boolean = false) { | ||||
|         if (newState == state && !force) { | ||||
|             Timber.v("State unchanged") | ||||
|             return | ||||
| @ -67,48 +62,26 @@ class KeysBackupBanner @JvmOverloads constructor( | ||||
| 
 | ||||
|         hideAll() | ||||
|         when (newState) { | ||||
|             State.Initial -> renderInitial() | ||||
|             State.Hidden -> renderHidden() | ||||
|             is State.Setup -> renderSetup(newState.numberOfKeys) | ||||
|             is State.Recover -> renderRecover(newState.version) | ||||
|             is State.Update -> renderUpdate(newState.version) | ||||
|             State.BackingUp -> renderBackingUp() | ||||
|             BannerState.Initial -> renderInitial() | ||||
|             BannerState.Hidden -> renderHidden() | ||||
|             is BannerState.Setup -> renderSetup(newState) | ||||
|             is BannerState.Recover -> renderRecover(newState) | ||||
|             is BannerState.Update -> renderUpdate(newState) | ||||
|             BannerState.BackingUp -> renderBackingUp() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onClick(v: View?) { | ||||
|         when (state) { | ||||
|             is State.Setup -> delegate?.setupKeysBackup() | ||||
|             is State.Update, | ||||
|             is State.Recover -> delegate?.recoverKeysBackup() | ||||
|             is BannerState.Setup -> delegate?.setupKeysBackup() | ||||
|             is BannerState.Update, | ||||
|             is BannerState.Recover -> delegate?.recoverKeysBackup() | ||||
|             else -> Unit | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun onCloseClicked() { | ||||
|         state.let { | ||||
|             when (it) { | ||||
|                 is State.Setup -> { | ||||
|                     DefaultSharedPreferences.getInstance(context).edit { | ||||
|                         putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true) | ||||
|                     } | ||||
|                 } | ||||
|                 is State.Recover -> { | ||||
|                     DefaultSharedPreferences.getInstance(context).edit { | ||||
|                         putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version) | ||||
|                     } | ||||
|                 } | ||||
|                 is State.Update -> { | ||||
|                     DefaultSharedPreferences.getInstance(context).edit { | ||||
|                         putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, it.version) | ||||
|                     } | ||||
|                 } | ||||
|                 else -> { | ||||
|                     // Should not happen, close button is not displayed in other cases | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         delegate?.onCloseClicked() | ||||
|         // Force refresh | ||||
|         render(state, true) | ||||
|     } | ||||
| @ -133,9 +106,8 @@ class KeysBackupBanner @JvmOverloads constructor( | ||||
|         isVisible = false | ||||
|     } | ||||
| 
 | ||||
|     private fun renderSetup(nbOfKeys: Int) { | ||||
|         if (nbOfKeys == 0 || | ||||
|                 DefaultSharedPreferences.getInstance(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) { | ||||
|     private fun renderSetup(state: BannerState.Setup) { | ||||
|         if (state.numberOfKeys == 0 || state.doNotShowAgain) { | ||||
|             // Do not display the setup banner if there is no keys to backup, or if the user has already closed it | ||||
|             isVisible = false | ||||
|         } else { | ||||
| @ -148,8 +120,8 @@ class KeysBackupBanner @JvmOverloads constructor( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun renderRecover(version: String) { | ||||
|         if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) { | ||||
|     private fun renderRecover(state: BannerState.Recover) { | ||||
|         if (state.version == state.doNotShowForVersion) { | ||||
|             isVisible = false | ||||
|         } else { | ||||
|             isVisible = true | ||||
| @ -161,8 +133,8 @@ class KeysBackupBanner @JvmOverloads constructor( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun renderUpdate(version: String) { | ||||
|         if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) { | ||||
|     private fun renderUpdate(state: BannerState.Update) { | ||||
|         if (state.version == state.doNotShowForVersion) { | ||||
|             isVisible = false | ||||
|         } else { | ||||
|             isVisible = true | ||||
| @ -191,61 +163,12 @@ class KeysBackupBanner @JvmOverloads constructor( | ||||
|         views.viewKeysBackupBannerLoading.isVisible = false | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The state representing the view. | ||||
|      * It can take one state at a time. | ||||
|      */ | ||||
|     sealed class State { | ||||
|         // Not yet rendered | ||||
|         object Initial : State() | ||||
| 
 | ||||
|         // View will be Gone | ||||
|         object Hidden : State() | ||||
| 
 | ||||
|         // Keys backup is not setup, numberOfKeys is the number of locally stored keys | ||||
|         data class Setup(val numberOfKeys: Int) : State() | ||||
| 
 | ||||
|         // Keys backup can be recovered, with version from the server | ||||
|         data class Recover(val version: String) : State() | ||||
| 
 | ||||
|         // Keys backup can be updated | ||||
|         data class Update(val version: String) : State() | ||||
| 
 | ||||
|         // Keys are backing up | ||||
|         object BackingUp : State() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * An interface to delegate some actions to another object. | ||||
|      */ | ||||
|     interface Delegate { | ||||
|         fun onCloseClicked() | ||||
|         fun setupKeysBackup() | ||||
|         fun recoverKeysBackup() | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         /** | ||||
|          * Preference key for setup. Value is a boolean. | ||||
|          */ | ||||
|         private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN" | ||||
| 
 | ||||
|         /** | ||||
|          * Preference key for recover. Value is a backup version (String). | ||||
|          */ | ||||
|         private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION" | ||||
| 
 | ||||
|         /** | ||||
|          * Preference key for update. Value is a backup version (String). | ||||
|          */ | ||||
|         private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION" | ||||
| 
 | ||||
|         /** | ||||
|          * Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version. | ||||
|          */ | ||||
|         fun onRecoverDoneForVersion(context: Context, version: String) { | ||||
|             DefaultSharedPreferences.getInstance(context).edit { | ||||
|                 putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -17,103 +17,109 @@ | ||||
| package im.vector.app.core.utils | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import android.media.Ringtone | ||||
| import android.media.RingtoneManager | ||||
| import android.net.Uri | ||||
| import androidx.core.content.edit | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.features.settings.VectorPreferences | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * This file manages the sound ringtone for calls. | ||||
|  * It allows you to use the default Riot Ringtone, or the standard ringtone or set a different one from the available choices | ||||
|  * This class manages the sound ringtone for calls. | ||||
|  * It allows you to use the default Element Ringtone, or the standard ringtone or set a different one from the available choices | ||||
|  * in Android. | ||||
|  */ | ||||
| class RingtoneUtils @Inject constructor( | ||||
|         @DefaultPreferences | ||||
|         private val sharedPreferences: SharedPreferences, | ||||
|         private val context: Context, | ||||
| ) { | ||||
|     /** | ||||
|      * Returns a Uri object that points to a specific Ringtone. | ||||
|      * | ||||
|      * If no Ringtone was explicitly set using Riot, it will return the Uri for the current system | ||||
|      * ringtone for calls. | ||||
|      * | ||||
|      * @return the [Uri] of the currently set [Ringtone] | ||||
|      * @see Ringtone | ||||
|      */ | ||||
|     fun getCallRingtoneUri(): Uri? { | ||||
|         val callRingtone: String? = sharedPreferences | ||||
|                 .getString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null) | ||||
| 
 | ||||
| /** | ||||
|  * Returns a Uri object that points to a specific Ringtone. | ||||
|  * | ||||
|  * If no Ringtone was explicitly set using Riot, it will return the Uri for the current system | ||||
|  * ringtone for calls. | ||||
|  * | ||||
|  * @return the [Uri] of the currently set [Ringtone] | ||||
|  * @see Ringtone | ||||
|  */ | ||||
| fun getCallRingtoneUri(context: Context): Uri? { | ||||
|     val callRingtone: String? = DefaultSharedPreferences.getInstance(context) | ||||
|             .getString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null) | ||||
|         callRingtone?.let { | ||||
|             return Uri.parse(it) | ||||
|         } | ||||
| 
 | ||||
|     callRingtone?.let { | ||||
|         return Uri.parse(it) | ||||
|         return try { | ||||
|             // Use current system notification sound for incoming calls per default (note that it can return null) | ||||
|             RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) | ||||
|         } catch (e: SecurityException) { | ||||
|             // Ignore for now | ||||
|             null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return try { | ||||
|         // Use current system notification sound for incoming calls per default (note that it can return null) | ||||
|         RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) | ||||
|     } catch (e: SecurityException) { | ||||
|         // Ignore for now | ||||
|         null | ||||
|     } | ||||
| } | ||||
|     /** | ||||
|      * Returns a Ringtone object that can then be played. | ||||
|      * | ||||
|      * If no Ringtone was explicitly set using Riot, it will return the current system ringtone | ||||
|      * for calls. | ||||
|      * | ||||
|      * @return the currently set [Ringtone] | ||||
|      * @see Ringtone | ||||
|      */ | ||||
|     fun getCallRingtone(): Ringtone? { | ||||
|         getCallRingtoneUri()?.let { | ||||
|             // Note that it can also return null | ||||
|             return RingtoneManager.getRingtone(context, it) | ||||
|         } | ||||
| 
 | ||||
| /** | ||||
|  * Returns a Ringtone object that can then be played. | ||||
|  * | ||||
|  * If no Ringtone was explicitly set using Riot, it will return the current system ringtone | ||||
|  * for calls. | ||||
|  * | ||||
|  * @return the currently set [Ringtone] | ||||
|  * @see Ringtone | ||||
|  */ | ||||
| fun getCallRingtone(context: Context): Ringtone? { | ||||
|     getCallRingtoneUri(context)?.let { | ||||
|         // Note that it can also return null | ||||
|         return RingtoneManager.getRingtone(context, it) | ||||
|         return null | ||||
|     } | ||||
| 
 | ||||
|     return null | ||||
| } | ||||
|     /** | ||||
|      * Returns a String with the name of the current Ringtone. | ||||
|      * | ||||
|      * If no Ringtone was explicitly set using Riot, it will return the name of the current system | ||||
|      * ringtone for calls. | ||||
|      * | ||||
|      * @return the name of the currently set [Ringtone], or null | ||||
|      * @see Ringtone | ||||
|      */ | ||||
|     fun getCallRingtoneName(): String? { | ||||
|         return getCallRingtone()?.getTitle(context) | ||||
|     } | ||||
| 
 | ||||
| /** | ||||
|  * Returns a String with the name of the current Ringtone. | ||||
|  * | ||||
|  * If no Ringtone was explicitly set using Riot, it will return the name of the current system | ||||
|  * ringtone for calls. | ||||
|  * | ||||
|  * @return the name of the currently set [Ringtone], or null | ||||
|  * @see Ringtone | ||||
|  */ | ||||
| fun getCallRingtoneName(context: Context): String? { | ||||
|     return getCallRingtone(context)?.getTitle(context) | ||||
| } | ||||
|     /** | ||||
|      * Sets the selected ringtone for riot calls. | ||||
|      * | ||||
|      * @param ringtoneUri | ||||
|      * @see Ringtone | ||||
|      */ | ||||
|     fun setCallRingtoneUri(ringtoneUri: Uri) { | ||||
|         sharedPreferences | ||||
|                 .edit { | ||||
|                     putString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString()) | ||||
|                 } | ||||
|     } | ||||
| 
 | ||||
| /** | ||||
|  * Sets the selected ringtone for riot calls. | ||||
|  * | ||||
|  * @param context Android context | ||||
|  * @param ringtoneUri | ||||
|  * @see Ringtone | ||||
|  */ | ||||
| fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) { | ||||
|     DefaultSharedPreferences.getInstance(context) | ||||
|             .edit { | ||||
|                 putString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString()) | ||||
|             } | ||||
| } | ||||
|     /** | ||||
|      * Set using Riot default ringtone. | ||||
|      */ | ||||
|     fun useRiotDefaultRingtone(): Boolean { | ||||
|         return sharedPreferences.getBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true) | ||||
|     } | ||||
| 
 | ||||
| /** | ||||
|  * Set using Riot default ringtone. | ||||
|  */ | ||||
| fun useRiotDefaultRingtone(context: Context): Boolean { | ||||
|     return DefaultSharedPreferences.getInstance(context).getBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true) | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Ask if default Riot ringtone has to be used. | ||||
|  */ | ||||
| fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) { | ||||
|     DefaultSharedPreferences.getInstance(context) | ||||
|             .edit { | ||||
|                 putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault) | ||||
|             } | ||||
|     /** | ||||
|      * Ask if default Riot ringtone has to be used. | ||||
|      */ | ||||
|     fun setUseRiotDefaultRingtone(useRiotDefault: Boolean) { | ||||
|         sharedPreferences | ||||
|                 .edit { | ||||
|                     putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault) | ||||
|                 } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -25,7 +25,7 @@ import im.vector.app.core.utils.toBase32String | ||||
| import im.vector.app.features.call.conference.jwt.JitsiJWTFactory | ||||
| import im.vector.app.features.displayname.getBestName | ||||
| import im.vector.app.features.raw.wellknown.getElementWellknown | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import im.vector.app.features.themes.ThemeProvider | ||||
| import okhttp3.Request | ||||
| import org.jitsi.meet.sdk.JitsiMeetUserInfo | ||||
| @ -49,6 +49,7 @@ class JitsiService @Inject constructor( | ||||
|         private val themeProvider: ThemeProvider, | ||||
|         private val jitsiJWTFactory: JitsiJWTFactory, | ||||
|         private val clock: Clock, | ||||
|         private val vectorLocale: VectorLocaleProvider, | ||||
| ) { | ||||
| 
 | ||||
|     companion object { | ||||
| @ -163,7 +164,7 @@ class JitsiService @Inject constructor( | ||||
|             if (widgetSessionId.length > 8) { | ||||
|                 widgetSessionId = widgetSessionId.substring(0, 7) | ||||
|             } | ||||
|             roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale) | ||||
|             roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(vectorLocale.applicationLocale) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -20,12 +20,15 @@ import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import dagger.hilt.android.AndroidEntryPoint | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.extensions.addChildFragment | ||||
| import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment | ||||
| import im.vector.app.databinding.BottomSheetCallDialPadBinding | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @AndroidEntryPoint | ||||
| class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialPadBinding>() { | ||||
| 
 | ||||
|     companion object { | ||||
| @ -41,6 +44,8 @@ class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCa | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Inject lateinit var vectorLocale: VectorLocaleProvider | ||||
| 
 | ||||
|     override val showExpanded = true | ||||
| 
 | ||||
|     var callback: DialPadFragment.Callback? = null | ||||
| @ -62,7 +67,7 @@ class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCa | ||||
|                     putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, showActions) | ||||
|                     putBoolean(DialPadFragment.EXTRA_ENABLE_OK, showActions) | ||||
|                     putBoolean(DialPadFragment.EXTRA_CURSOR_VISIBLE, false) | ||||
|                     putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country) | ||||
|                     putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country) | ||||
|                 } | ||||
|                 callback = DialPadFragmentCallbackWrapper(this@CallDialPadBottomSheet.callback) | ||||
|             }.also { | ||||
|  | ||||
| @ -28,7 +28,6 @@ import im.vector.app.core.extensions.addFragment | ||||
| import im.vector.app.core.platform.SimpleFragmentActivity | ||||
| import im.vector.app.features.call.webrtc.WebRtcCallManager | ||||
| import im.vector.app.features.createdirect.DirectRoomHelper | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog | ||||
| import kotlinx.coroutines.launch | ||||
| import org.matrix.android.sdk.api.session.Session | ||||
| @ -78,7 +77,7 @@ class PstnDialActivity : SimpleFragmentActivity() { | ||||
|             arguments = Bundle().apply { | ||||
|                 putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true) | ||||
|                 putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true) | ||||
|                 putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country) | ||||
|                 putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country) | ||||
|             } | ||||
|             callback = object : DialPadFragment.Callback { | ||||
|                 override fun onOkClicked(formatted: String?, raw: String?) { | ||||
|  | ||||
| @ -59,7 +59,7 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>() { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         sectionsPagerAdapter = CallTransferPagerAdapter(this) | ||||
|         sectionsPagerAdapter = CallTransferPagerAdapter(this, vectorLocale) | ||||
|         views.callTransferViewPager.adapter = sectionsPagerAdapter | ||||
| 
 | ||||
|         TabLayoutMediator(views.callTransferTabLayout, views.callTransferViewPager) { tab, position -> | ||||
|  | ||||
| @ -22,12 +22,13 @@ import androidx.fragment.app.FragmentActivity | ||||
| import androidx.viewpager2.adapter.FragmentStateAdapter | ||||
| import im.vector.app.core.extensions.toMvRxBundle | ||||
| import im.vector.app.features.call.dialpad.DialPadFragment | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import im.vector.app.features.userdirectory.UserListFragment | ||||
| import im.vector.app.features.userdirectory.UserListFragmentArgs | ||||
| 
 | ||||
| class CallTransferPagerAdapter( | ||||
|         private val fragmentActivity: FragmentActivity | ||||
|         private val fragmentActivity: FragmentActivity, | ||||
|         private val vectorLocale: VectorLocaleProvider, | ||||
| ) : FragmentStateAdapter(fragmentActivity) { | ||||
| 
 | ||||
|     companion object { | ||||
| @ -61,7 +62,7 @@ class CallTransferPagerAdapter( | ||||
|                 arguments = Bundle().apply { | ||||
|                     putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true) | ||||
|                     putBoolean(DialPadFragment.EXTRA_ENABLE_OK, false) | ||||
|                     putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country) | ||||
|                     putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -22,7 +22,7 @@ import android.os.Build | ||||
| import android.os.LocaleList | ||||
| import androidx.annotation.RequiresApi | ||||
| import im.vector.app.features.settings.FontScalePreferences | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import im.vector.app.features.themes.ThemeUtils | ||||
| import timber.log.Timber | ||||
| import java.util.Locale | ||||
| @ -33,21 +33,22 @@ import javax.inject.Inject | ||||
|  */ | ||||
| class VectorConfiguration @Inject constructor( | ||||
|         private val context: Context, | ||||
|         private val fontScalePreferences: FontScalePreferences | ||||
|         private val fontScalePreferences: FontScalePreferences, | ||||
|         private val vectorLocale: VectorLocaleProvider, | ||||
| ) { | ||||
| 
 | ||||
|     fun onConfigurationChanged() { | ||||
|         if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) { | ||||
|         if (Locale.getDefault().toString() != vectorLocale.applicationLocale.toString()) { | ||||
|             Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}") | ||||
|             Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}") | ||||
|             Locale.setDefault(VectorLocale.applicationLocale) | ||||
|             Timber.v("## onConfigurationChanged(): restore the expected value ${vectorLocale.applicationLocale}") | ||||
|             Locale.setDefault(vectorLocale.applicationLocale) | ||||
|         } | ||||
|         // Night mode may have changed | ||||
|         ThemeUtils.init(context) | ||||
|     } | ||||
| 
 | ||||
|     fun applyToApplicationContext() { | ||||
|         val locale = VectorLocale.applicationLocale | ||||
|         val locale = vectorLocale.applicationLocale | ||||
|         val fontScale = fontScalePreferences.getResolvedFontScaleValue() | ||||
| 
 | ||||
|         Locale.setDefault(locale) | ||||
| @ -67,7 +68,7 @@ class VectorConfiguration @Inject constructor( | ||||
|      */ | ||||
|     fun getLocalisedContext(context: Context): Context { | ||||
|         try { | ||||
|             val locale = VectorLocale.applicationLocale | ||||
|             val locale = vectorLocale.applicationLocale | ||||
| 
 | ||||
|             // create new configuration passing old configuration from original Context | ||||
|             val configuration = Configuration(context.resources.configuration) | ||||
| @ -107,7 +108,7 @@ class VectorConfiguration @Inject constructor( | ||||
|      * @return the local status value | ||||
|      */ | ||||
|     fun getHash(): String { | ||||
|         return (VectorLocale.applicationLocale.toString() + | ||||
|         return (vectorLocale.applicationLocale.toString() + | ||||
|                 "_" + fontScalePreferences.getResolvedFontScaleValue().preferenceValue + | ||||
|                 "_" + ThemeUtils.getApplicationTheme(context)) | ||||
|     } | ||||
|  | ||||
| @ -18,6 +18,7 @@ package im.vector.app.features.crypto.keysbackup.restore | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import com.airbnb.mvrx.viewModel | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import dagger.hilt.android.AndroidEntryPoint | ||||
| import im.vector.app.R | ||||
| @ -27,8 +28,9 @@ import im.vector.app.core.extensions.observeEvent | ||||
| import im.vector.app.core.extensions.registerStartForActivityResult | ||||
| import im.vector.app.core.extensions.replaceFragment | ||||
| import im.vector.app.core.platform.SimpleFragmentActivity | ||||
| import im.vector.app.core.ui.views.KeysBackupBanner | ||||
| import im.vector.app.features.crypto.quads.SharedSecureStorageActivity | ||||
| import im.vector.app.features.workers.signout.ServerBackupStatusAction | ||||
| import im.vector.app.features.workers.signout.ServerBackupStatusViewModel | ||||
| import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @ -46,6 +48,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { | ||||
|     override fun getTitleRes() = R.string.title_activity_keys_backup_restore | ||||
| 
 | ||||
|     private lateinit var viewModel: KeysBackupRestoreSharedViewModel | ||||
|     private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() | ||||
| 
 | ||||
|     override fun onBackPressed() { | ||||
|         hideWaitingView() | ||||
| @ -95,7 +98,8 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { | ||||
|                 } | ||||
|                 KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> { | ||||
|                     viewModel.keyVersionResult.value?.version?.let { | ||||
|                         KeysBackupBanner.onRecoverDoneForVersion(this, it) | ||||
|                         // Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version. | ||||
|                         serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnRecoverDoneForVersion(it)) | ||||
|                     } | ||||
|                     replaceFragment(views.container, KeysBackupRestoreSuccessFragment::class.java, allowStateLoss = true) | ||||
|                 } | ||||
|  | ||||
| @ -29,9 +29,10 @@ import im.vector.app.R | ||||
| import im.vector.app.core.extensions.hidePassword | ||||
| import im.vector.app.core.platform.VectorBaseFragment | ||||
| import im.vector.app.databinding.FragmentKeysBackupSetupStep2Binding | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @AndroidEntryPoint | ||||
| class KeysBackupSetupStep2Fragment : | ||||
| @ -43,6 +44,8 @@ class KeysBackupSetupStep2Fragment : | ||||
| 
 | ||||
|     private val zxcvbn = Zxcvbn() | ||||
| 
 | ||||
|     @Inject lateinit var vectorLocale: VectorLocaleProvider | ||||
| 
 | ||||
|     private fun onPassphraseChanged() { | ||||
|         viewModel.passphrase.value = views.keysBackupSetupStep2PassphraseEnterEdittext.text.toString() | ||||
|         viewModel.confirmPassphraseError.value = null | ||||
| @ -78,12 +81,12 @@ class KeysBackupSetupStep2Fragment : | ||||
|                 views.keysBackupSetupStep2PassphraseStrengthLevel.strength = score | ||||
| 
 | ||||
|                 if (score in 1..3) { | ||||
|                     val warning = strength.feedback?.getWarning(VectorLocale.applicationLocale) | ||||
|                     val warning = strength.feedback?.getWarning(vectorLocale.applicationLocale) | ||||
|                     if (warning != null) { | ||||
|                         views.keysBackupSetupStep2PassphraseEnterTil.error = warning | ||||
|                     } | ||||
| 
 | ||||
|                     val suggestions = strength.feedback?.getSuggestions(VectorLocale.applicationLocale) | ||||
|                     val suggestions = strength.feedback?.getSuggestions(vectorLocale.applicationLocale) | ||||
|                     if (suggestions != null) { | ||||
|                         views.keysBackupSetupStep2PassphraseEnterTil.error = suggestions.firstOrNull() | ||||
|                     } | ||||
|  | ||||
| @ -28,12 +28,13 @@ import dagger.hilt.android.AndroidEntryPoint | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.platform.VectorBaseFragment | ||||
| import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import im.vector.lib.core.utils.flow.throttleFirst | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import reactivecircus.flowbinding.android.widget.editorActionEvents | ||||
| import reactivecircus.flowbinding.android.widget.textChanges | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @AndroidEntryPoint | ||||
| class BootstrapEnterPassphraseFragment : | ||||
| @ -43,6 +44,8 @@ class BootstrapEnterPassphraseFragment : | ||||
|         return FragmentBootstrapEnterPassphraseBinding.inflate(inflater, container, false) | ||||
|     } | ||||
| 
 | ||||
|     @Inject lateinit var vectorLocale: VectorLocaleProvider | ||||
| 
 | ||||
|     val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() | ||||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
| @ -105,8 +108,8 @@ class BootstrapEnterPassphraseFragment : | ||||
|                 views.ssssPassphraseSecurityProgress.strength = score | ||||
|                 if (score in 1..3) { | ||||
|                     val hint = | ||||
|                             strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() } | ||||
|                                     ?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull() | ||||
|                             strength.feedback?.getWarning(vectorLocale.applicationLocale)?.takeIf { it.isNotBlank() } | ||||
|                                     ?: strength.feedback?.getSuggestions(vectorLocale.applicationLocale)?.firstOrNull() | ||||
|                     if (hint != null && hint != views.ssssPassphraseEnterTil.error.toString()) { | ||||
|                         views.ssssPassphraseEnterTil.error = hint | ||||
|                     } | ||||
|  | ||||
| @ -17,44 +17,46 @@ | ||||
| package im.vector.app.features.disclaimer | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import androidx.core.content.edit | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.core.utils.openUrlInChromeCustomTab | ||||
| import im.vector.app.features.settings.VectorSettingsUrls | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| // Increase this value to show again the disclaimer dialog after an upgrade of the application | ||||
| private const val CURRENT_DISCLAIMER_VALUE = 2 | ||||
| 
 | ||||
| const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE" | ||||
| 
 | ||||
| fun showDisclaimerDialog(activity: Activity) { | ||||
|     val sharedPrefs = DefaultSharedPreferences.getInstance(activity) | ||||
| class DisclaimerDialog @Inject constructor( | ||||
|         @DefaultPreferences | ||||
|         private val sharedPrefs: SharedPreferences, | ||||
| ) { | ||||
|     fun showDisclaimerDialog(activity: Activity) { | ||||
|         if (sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE) { | ||||
|             sharedPrefs.edit { | ||||
|                 putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) | ||||
|             } | ||||
| 
 | ||||
|     if (sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE) { | ||||
|             val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_disclaimer_content, null) | ||||
| 
 | ||||
|             MaterialAlertDialogBuilder(activity) | ||||
|                     .setView(dialogLayout) | ||||
|                     .setCancelable(false) | ||||
|                     .setNegativeButton(R.string.disclaimer_negative_button, null) | ||||
|                     .setPositiveButton(R.string.disclaimer_positive_button) { _, _ -> | ||||
|                         openUrlInChromeCustomTab(activity, null, VectorSettingsUrls.DISCLAIMER_URL) | ||||
|                     } | ||||
|                     .show() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun doNotShowDisclaimerDialog() { | ||||
|         sharedPrefs.edit { | ||||
|             putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) | ||||
|         } | ||||
| 
 | ||||
|         val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_disclaimer_content, null) | ||||
| 
 | ||||
|         MaterialAlertDialogBuilder(activity) | ||||
|                 .setView(dialogLayout) | ||||
|                 .setCancelable(false) | ||||
|                 .setNegativeButton(R.string.disclaimer_negative_button, null) | ||||
|                 .setPositiveButton(R.string.disclaimer_positive_button) { _, _ -> | ||||
|                     openUrlInChromeCustomTab(activity, null, VectorSettingsUrls.DISCLAIMER_URL) | ||||
|                 } | ||||
|                 .show() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fun doNotShowDisclaimerDialog(context: Context) { | ||||
|     val sharedPrefs = DefaultSharedPreferences.getInstance(context) | ||||
| 
 | ||||
|     sharedPrefs.edit { | ||||
|         putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -56,7 +56,7 @@ import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewMode | ||||
| import im.vector.app.features.analytics.plan.MobileScreen | ||||
| import im.vector.app.features.analytics.plan.ViewRoom | ||||
| import im.vector.app.features.crypto.recover.SetupMode | ||||
| import im.vector.app.features.disclaimer.showDisclaimerDialog | ||||
| import im.vector.app.features.disclaimer.DisclaimerDialog | ||||
| import im.vector.app.features.home.room.list.actions.RoomListSharedAction | ||||
| import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel | ||||
| import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment | ||||
| @ -141,6 +141,7 @@ class HomeActivity : | ||||
|     @Inject lateinit var unifiedPushHelper: UnifiedPushHelper | ||||
|     @Inject lateinit var fcmHelper: FcmHelper | ||||
|     @Inject lateinit var nightlyProxy: NightlyProxy | ||||
|     @Inject lateinit var disclaimerDialog: DisclaimerDialog | ||||
| 
 | ||||
|     private var isNewAppLayoutEnabled: Boolean = false // delete once old app layout is removed | ||||
| 
 | ||||
| @ -570,7 +571,7 @@ class HomeActivity : | ||||
|                     .setNegativeButton(R.string.no) { _, _ -> bugReporter.deleteCrashFile() } | ||||
|                     .show() | ||||
|         } else { | ||||
|             showDisclaimerDialog(this) | ||||
|             disclaimerDialog.showDisclaimerDialog(this) | ||||
|         } | ||||
| 
 | ||||
|         // Force remote backup state update to update the banner if needed | ||||
|  | ||||
| @ -51,11 +51,12 @@ import im.vector.app.features.home.room.list.RoomListParams | ||||
| import im.vector.app.features.home.room.list.UnreadCounterBadgeView | ||||
| import im.vector.app.features.popup.PopupAlertManager | ||||
| import im.vector.app.features.popup.VerificationVectorAlert | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import im.vector.app.features.settings.VectorPreferences | ||||
| import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS | ||||
| import im.vector.app.features.themes.ThemeUtils | ||||
| import im.vector.app.features.workers.signout.BannerState | ||||
| import im.vector.app.features.workers.signout.ServerBackupStatusAction | ||||
| import im.vector.app.features.workers.signout.ServerBackupStatusViewModel | ||||
| import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo | ||||
| import org.matrix.android.sdk.api.session.room.model.RoomSummary | ||||
| @ -75,6 +76,7 @@ class HomeDetailFragment : | ||||
|     @Inject lateinit var callManager: WebRtcCallManager | ||||
|     @Inject lateinit var vectorPreferences: VectorPreferences | ||||
|     @Inject lateinit var spaceStateHandler: SpaceStateHandler | ||||
|     @Inject lateinit var vectorLocale: VectorLocaleProvider | ||||
| 
 | ||||
|     private val viewModel: HomeDetailViewModel by fragmentViewModel() | ||||
|     private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() | ||||
| @ -288,13 +290,15 @@ class HomeDetailFragment : | ||||
|     } | ||||
| 
 | ||||
|     private fun setupKeysBackupBanner() { | ||||
|         serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerDisplayed) | ||||
|         serverBackupStatusViewModel | ||||
|                 .onEach { | ||||
|                     when (val banState = it.bannerState.invoke()) { | ||||
|                         is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) | ||||
|                         BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) | ||||
|                         null, | ||||
|                         BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) | ||||
|                         is BannerState.Setup, | ||||
|                         BannerState.BackingUp, | ||||
|                         BannerState.Hidden -> views.homeKeysBackupBanner.render(banState, false) | ||||
|                         null -> views.homeKeysBackupBanner.render(BannerState.Hidden, false) | ||||
|                         else -> Unit /* No op? */ | ||||
|                     } | ||||
|                 } | ||||
|         views.homeKeysBackupBanner.delegate = this | ||||
| @ -378,7 +382,7 @@ class HomeDetailFragment : | ||||
|             arguments = Bundle().apply { | ||||
|                 putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true) | ||||
|                 putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true) | ||||
|                 putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country) | ||||
|                 putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country) | ||||
|             } | ||||
|             applyCallback() | ||||
|         } | ||||
| @ -401,6 +405,10 @@ class HomeDetailFragment : | ||||
|      * KeysBackupBanner Listener | ||||
|      * ========================================================================================== */ | ||||
| 
 | ||||
|     override fun onCloseClicked() { | ||||
|         serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed) | ||||
|     } | ||||
| 
 | ||||
|     override fun setupKeysBackup() { | ||||
|         navigator.openKeysBackupSetup(requireActivity(), false) | ||||
|     } | ||||
|  | ||||
| @ -57,6 +57,7 @@ import im.vector.app.features.settings.VectorPreferences | ||||
| import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS | ||||
| import im.vector.app.features.spaces.SpaceListBottomSheet | ||||
| import im.vector.app.features.workers.signout.BannerState | ||||
| import im.vector.app.features.workers.signout.ServerBackupStatusAction | ||||
| import im.vector.app.features.workers.signout.ServerBackupStatusViewModel | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| @ -300,13 +301,15 @@ class NewHomeDetailFragment : | ||||
|     } | ||||
| 
 | ||||
|     private fun setupKeysBackupBanner() { | ||||
|         serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerDisplayed) | ||||
|         serverBackupStatusViewModel | ||||
|                 .onEach { | ||||
|                     when (val banState = it.bannerState.invoke()) { | ||||
|                         is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) | ||||
|                         BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) | ||||
|                         null, | ||||
|                         BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) | ||||
|                         is BannerState.Setup, | ||||
|                         BannerState.BackingUp, | ||||
|                         BannerState.Hidden -> views.homeKeysBackupBanner.render(banState, false) | ||||
|                         null -> views.homeKeysBackupBanner.render(BannerState.Hidden, false) | ||||
|                         else -> Unit /* No op? */ | ||||
|                     } | ||||
|                 } | ||||
|         views.homeKeysBackupBanner.delegate = this | ||||
| @ -348,6 +351,10 @@ class NewHomeDetailFragment : | ||||
|      * KeysBackupBanner Listener | ||||
|      * ========================================================================================== */ | ||||
| 
 | ||||
|     override fun onCloseClicked() { | ||||
|         serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed) | ||||
|     } | ||||
| 
 | ||||
|     override fun setupKeysBackup() { | ||||
|         navigator.openKeysBackupSetup(requireActivity(), false) | ||||
|     } | ||||
|  | ||||
| @ -20,6 +20,7 @@ import android.content.Context | ||||
| import android.content.pm.ShortcutInfo | ||||
| import android.graphics.Bitmap | ||||
| import android.os.Build | ||||
| import androidx.annotation.ChecksSdkIntAtLeast | ||||
| import androidx.annotation.WorkerThread | ||||
| import androidx.core.content.pm.ShortcutInfoCompat | ||||
| import androidx.core.content.pm.ShortcutManagerCompat | ||||
| @ -32,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary | ||||
| import org.matrix.android.sdk.api.util.toMatrixItem | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) | ||||
| private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O | ||||
| private const val adaptiveIconSizeDp = 108 | ||||
| private const val adaptiveIconOuterSidesDp = 18 | ||||
|  | ||||
| @ -1124,6 +1124,7 @@ class TimelineFragment : | ||||
|                         .findViewById<ImageView>(R.id.action_view_icon_image) | ||||
|                         .setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary)) | ||||
|                 actionView.findViewById<TextView>(R.id.cart_badge).setTextOrHide("$widgetsCount") | ||||
|                 @Suppress("AlwaysShowAction") | ||||
|                 matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) | ||||
|             } | ||||
| 
 | ||||
|  | ||||
| @ -16,29 +16,36 @@ | ||||
| 
 | ||||
| package im.vector.app.features.homeserver | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import androidx.core.content.edit | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.core.resources.StringProvider | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * Object to store and retrieve home and identity server urls. | ||||
|  */ | ||||
| object ServerUrlsRepository { | ||||
| class ServerUrlsRepository @Inject constructor( | ||||
|         @DefaultPreferences | ||||
|         private val sharedPreferences: SharedPreferences, | ||||
|         private val stringProvider: StringProvider, | ||||
| ) { | ||||
|     companion object { | ||||
|         // Keys used to store default servers urls from the referrer | ||||
|         private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url" | ||||
|         private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url" | ||||
| 
 | ||||
|     // Keys used to store default servers urls from the referrer | ||||
|     private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url" | ||||
|     private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url" | ||||
| 
 | ||||
|     // Keys used to store current homeserver url and identity url | ||||
|     const val HOME_SERVER_URL_PREF = "home_server_url" | ||||
|     const val IDENTITY_SERVER_URL_PREF = "identity_server_url" | ||||
|         // Keys used to store current homeserver url and identity url | ||||
|         const val HOME_SERVER_URL_PREF = "home_server_url" | ||||
|         const val IDENTITY_SERVER_URL_PREF = "identity_server_url" | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save home and identity sever urls received by the Referrer receiver. | ||||
|      */ | ||||
|     fun setDefaultUrlsFromReferrer(context: Context, homeServerUrl: String, identityServerUrl: String) { | ||||
|         DefaultSharedPreferences.getInstance(context) | ||||
|     fun setDefaultUrlsFromReferrer(homeServerUrl: String, identityServerUrl: String) { | ||||
|         sharedPreferences | ||||
|                 .edit { | ||||
|                     if (homeServerUrl.isNotEmpty()) { | ||||
|                         putString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, homeServerUrl) | ||||
| @ -53,8 +60,8 @@ object ServerUrlsRepository { | ||||
|     /** | ||||
|      * Save home and identity sever urls entered by the user. May be custom or default value. | ||||
|      */ | ||||
|     fun saveServerUrls(context: Context, homeServerUrl: String, identityServerUrl: String) { | ||||
|         DefaultSharedPreferences.getInstance(context) | ||||
|     fun saveServerUrls(homeServerUrl: String, identityServerUrl: String) { | ||||
|         sharedPreferences | ||||
|                 .edit { | ||||
|                     putString(HOME_SERVER_URL_PREF, homeServerUrl) | ||||
|                     putString(IDENTITY_SERVER_URL_PREF, identityServerUrl) | ||||
| @ -64,14 +71,12 @@ object ServerUrlsRepository { | ||||
|     /** | ||||
|      * Return last used homeserver url, or the default one from referrer or the default one from resources. | ||||
|      */ | ||||
|     fun getLastHomeServerUrl(context: Context): String { | ||||
|         val prefs = DefaultSharedPreferences.getInstance(context) | ||||
| 
 | ||||
|         return prefs.getString( | ||||
|     fun getLastHomeServerUrl(): String { | ||||
|         return sharedPreferences.getString( | ||||
|                 HOME_SERVER_URL_PREF, | ||||
|                 prefs.getString( | ||||
|                 sharedPreferences.getString( | ||||
|                         DEFAULT_REFERRER_HOME_SERVER_URL_PREF, | ||||
|                         getDefaultHomeServerUrl(context) | ||||
|                         getDefaultHomeServerUrl() | ||||
|                 )!! | ||||
|         )!! | ||||
|     } | ||||
| @ -79,10 +84,10 @@ object ServerUrlsRepository { | ||||
|     /** | ||||
|      * Return true if url is the default homeserver url form resources. | ||||
|      */ | ||||
|     fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context) | ||||
|     fun isDefaultHomeServerUrl(url: String) = url == getDefaultHomeServerUrl() | ||||
| 
 | ||||
|     /** | ||||
|      * Return default homeserver url from resources. | ||||
|      */ | ||||
|     fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.matrix_org_server_url) | ||||
|     fun getDefaultHomeServerUrl() = stringProvider.getString(R.string.matrix_org_server_url) | ||||
| } | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| package im.vector.app.features.lifecycle | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.Activity | ||||
| import android.app.ActivityManager | ||||
| import android.app.Application | ||||
| @ -91,7 +90,6 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager | ||||
|      * | ||||
|      * @return true if an app task is corrupted by a potentially malicious activity | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     private suspend fun isTaskCorrupted(activity: Activity): Boolean = withContext(Dispatchers.Default) { | ||||
|         val context = activity.applicationContext | ||||
|         val packageManager: PackageManager = context.packageManager | ||||
|  | ||||
| @ -144,7 +144,6 @@ class LoginCaptchaFragment : | ||||
|                 // runOnUiThread(Runnable { finish() }) | ||||
|             } | ||||
| 
 | ||||
|             @SuppressLint("NewApi") | ||||
|             override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { | ||||
|                 super.onReceivedHttpError(view, request, errorResponse) | ||||
| 
 | ||||
|  | ||||
| @ -19,7 +19,6 @@ | ||||
| package im.vector.app.features.notifications | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.annotation.TargetApi | ||||
| import android.app.Notification | ||||
| import android.app.NotificationChannel | ||||
| import android.app.NotificationManager | ||||
| @ -34,6 +33,7 @@ import android.text.Spannable | ||||
| import android.text.SpannableString | ||||
| import android.text.style.ForegroundColorSpan | ||||
| import androidx.annotation.AttrRes | ||||
| import androidx.annotation.ChecksSdkIntAtLeast | ||||
| import androidx.annotation.DrawableRes | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.core.app.NotificationCompat | ||||
| @ -102,6 +102,7 @@ class NotificationUtils @Inject constructor( | ||||
|         const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" | ||||
|         private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" | ||||
| 
 | ||||
|         @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) | ||||
|         fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||||
| 
 | ||||
|         fun openSystemSettingsForSilentCategory(fragment: Fragment) { | ||||
| @ -126,7 +127,6 @@ class NotificationUtils @Inject constructor( | ||||
|     /** | ||||
|      * Create notification channels. | ||||
|      */ | ||||
|     @TargetApi(Build.VERSION_CODES.O) | ||||
|     fun createNotificationChannels() { | ||||
|         if (!supportNotificationChannels()) { | ||||
|             return | ||||
| @ -218,7 +218,6 @@ class NotificationUtils @Inject constructor( | ||||
|      * @param withProgress true to show indeterminate progress on the notification | ||||
|      * @return the polling thread listener notification | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { | ||||
|         // build the pending intent go to the home screen if this is clicked. | ||||
|         val i = HomeActivity.newIntent(context, firstStartMainActivity = false) | ||||
| @ -287,7 +286,6 @@ class NotificationUtils @Inject constructor( | ||||
|      * @param fromBg true if the app is in background when posting the notification | ||||
|      * @return the call notification. | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     fun buildIncomingCallNotification( | ||||
|             call: WebRtcCall, | ||||
|             title: String, | ||||
| @ -420,7 +418,6 @@ class NotificationUtils @Inject constructor( | ||||
|      * @param title title of the notification | ||||
|      * @return the call notification. | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     fun buildPendingCallNotification( | ||||
|             call: WebRtcCall, | ||||
|             title: String | ||||
| @ -966,6 +963,7 @@ class NotificationUtils @Inject constructor( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("LaunchActivityFromNotification") | ||||
|     fun displayDiagnosticNotification() { | ||||
|         val testActionIntent = Intent(context, TestNotificationReceiver::class.java) | ||||
|         testActionIntent.action = actionIds.diagnostic | ||||
|  | ||||
| @ -92,7 +92,6 @@ class CaptchaWebview @Inject constructor( | ||||
|                 Timber.e("## onError() : $errorMessage") | ||||
|             } | ||||
| 
 | ||||
|             @SuppressLint("NewApi") | ||||
|             override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { | ||||
|                 super.onReceivedHttpError(view, request, errorResponse) | ||||
|                 when { | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| package im.vector.app.features.pin.lockscreen.biometrics | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.os.Build | ||||
| import androidx.annotation.MainThread | ||||
| @ -156,7 +155,6 @@ class BiometricHelper @AssistedInject constructor( | ||||
|         return authenticate(activity) | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("NewApi") | ||||
|     @OptIn(ExperimentalCoroutinesApi::class) | ||||
|     private fun authenticateInternal( | ||||
|         activity: FragmentActivity, | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| package im.vector.app.features.pin.lockscreen.crypto | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.os.Build | ||||
| import android.security.keystore.KeyPermanentlyInvalidatedException | ||||
| @ -55,7 +54,6 @@ class KeyStoreCrypto @AssistedInject constructor( | ||||
|      * Ensures a [Key] for the [alias] exists and validates it. | ||||
|      * @throws KeyPermanentlyInvalidatedException if key is not valid. | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     @Throws(KeyPermanentlyInvalidatedException::class) | ||||
|     fun ensureKey() = secretStoringUtils.ensureKey(alias).also { | ||||
|         // Check validity of Key by initializing an encryption Cipher | ||||
| @ -109,10 +107,9 @@ class KeyStoreCrypto @AssistedInject constructor( | ||||
|     /** | ||||
|      * Check if the key associated with the [alias] is valid. | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     fun hasValidKey(): Boolean { | ||||
|         val keyExists = hasKey() | ||||
|         return if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && keyExists) { | ||||
|         return if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) && keyExists) { | ||||
|             val initializedKey = tryOrNull("Error validating lockscreen system key.") { ensureKey() } | ||||
|             initializedKey != null | ||||
|         } else { | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| package im.vector.app.features.pin.lockscreen.crypto | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.os.Build | ||||
| import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator | ||||
| import im.vector.app.features.pin.lockscreen.crypto.migrations.MissingSystemKeyMigrator | ||||
| @ -36,14 +35,13 @@ class LockScreenKeysMigrator @Inject constructor( | ||||
|     /** | ||||
|      * Performs any needed migrations in order. | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     suspend fun migrateIfNeeded() { | ||||
|         if (legacyPinCodeMigrator.isMigrationNeeded()) { | ||||
|             legacyPinCodeMigrator.migrate() | ||||
|             missingSystemKeyMigrator.migrateIfNeeded() | ||||
|         } | ||||
| 
 | ||||
|         if (systemKeyV1Migrator.isMigrationNeeded() && versionProvider.get() >= Build.VERSION_CODES.M) { | ||||
|         if (systemKeyV1Migrator.isMigrationNeeded() && versionProvider.isAtLeast(Build.VERSION_CODES.M)) { | ||||
|             systemKeyV1Migrator.migrate() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| package im.vector.app.features.pin.lockscreen.crypto.migrations | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.os.Build | ||||
| import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto | ||||
| import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias | ||||
| @ -38,9 +37,9 @@ class MissingSystemKeyMigrator @Inject constructor( | ||||
|     /** | ||||
|      * If user had biometric auth enabled, ensure system key exists, creating one if needed. | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     fun migrateIfNeeded() { | ||||
|         if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && vectorPreferences.useBiometricsToUnlock()) { | ||||
|         if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) && | ||||
|                 vectorPreferences.useBiometricsToUnlock()) { | ||||
|             val systemKeyStoreCrypto = keystoreCryptoFactory.provide(systemKeyAlias, true) | ||||
|             runCatching { | ||||
|                 systemKeyStoreCrypto.ensureKey() | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| package im.vector.app.features.pin.lockscreen.ui | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.KeyguardManager | ||||
| import android.os.Build | ||||
| import android.security.keystore.KeyPermanentlyInvalidatedException | ||||
| @ -139,12 +138,12 @@ class LockScreenViewModel @AssistedInject constructor( | ||||
|         } | ||||
|     }.launchIn(viewModelScope) | ||||
| 
 | ||||
|     @SuppressLint("NewApi") | ||||
|     private fun showBiometricPrompt(activity: FragmentActivity) = flow { | ||||
|         emitAll(biometricHelper.authenticate(activity)) | ||||
|     }.catch { error -> | ||||
|         when { | ||||
|             versionProvider.get() >= Build.VERSION_CODES.M && error is KeyPermanentlyInvalidatedException -> { | ||||
|             versionProvider.isAtLeast(Build.VERSION_CODES.M) && | ||||
|                     error is KeyPermanentlyInvalidatedException -> { | ||||
|                 onBiometricKeyInvalidated() | ||||
|             } | ||||
|             else -> { | ||||
| @ -168,15 +167,14 @@ class LockScreenViewModel @AssistedInject constructor( | ||||
|         _viewEvents.post(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage) | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("NewApi") | ||||
|     private suspend fun updateStateWithBiometricInfo() { | ||||
|         // This is a terrible hack, but I found no other way to ensure this would be called only after the device is considered unlocked on Android 12+ | ||||
|         waitUntilKeyguardIsUnlocked() | ||||
|         setState { | ||||
|             val isBiometricKeyInvalidated = biometricHelper.hasSystemKey && !biometricHelper.isSystemKeyValid | ||||
|             val canUseBiometricAuth = lockScreenConfiguration.mode == LockScreenMode.VERIFY && | ||||
|                 !isSystemAuthTemporarilyDisabledByBiometricPrompt && | ||||
|                 biometricHelper.isSystemAuthEnabledAndValid | ||||
|                     !isSystemAuthTemporarilyDisabledByBiometricPrompt && | ||||
|                     biometricHelper.isSystemAuthEnabledAndValid | ||||
|             val showBiometricPromptAutomatically = canUseBiometricAuth && lockScreenConfiguration.autoStartBiometric | ||||
|             copy( | ||||
|                     canUseBiometricAuth = canUseBiometricAuth, | ||||
| @ -191,12 +189,12 @@ class LockScreenViewModel @AssistedInject constructor( | ||||
|      * after an Activity's `onResume` method. If we mix that with the system keys needing the device to be unlocked before they're used, we get crashes. | ||||
|      * See issue [#6768](https://github.com/vector-im/element-android/issues/6768). | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     private suspend fun waitUntilKeyguardIsUnlocked() { | ||||
|         if (versionProvider.get() < Build.VERSION_CODES.S) return | ||||
|         withTimeoutOrNull(5.seconds) { | ||||
|             while (keyguardManager.isDeviceLocked) { | ||||
|                 delay(50.milliseconds) | ||||
|         if (versionProvider.isAtLeast(Build.VERSION_CODES.S)) { | ||||
|             withTimeoutOrNull(5.seconds) { | ||||
|                 while (keyguardManager.isDeviceLocked) { | ||||
|                     delay(50.milliseconds) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -31,7 +31,7 @@ import im.vector.app.core.di.ActiveSessionHolder | ||||
| import im.vector.app.core.extensions.getAllChildFragments | ||||
| import im.vector.app.core.extensions.toOnOff | ||||
| import im.vector.app.core.resources.BuildMeta | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import im.vector.app.features.settings.VectorLocaleProvider | ||||
| import im.vector.app.features.settings.VectorPreferences | ||||
| import im.vector.app.features.settings.devtools.GossipingEventsSerializer | ||||
| import im.vector.app.features.settings.locale.SystemLocaleProvider | ||||
| @ -80,6 +80,7 @@ class BugReporter @Inject constructor( | ||||
|         private val buildMeta: BuildMeta, | ||||
|         private val processInfo: ProcessInfo, | ||||
|         private val sdkIntProvider: BuildVersionSdkIntProvider, | ||||
|         private val vectorLocale: VectorLocaleProvider, | ||||
| ) { | ||||
|     var inMultiWindowMode = false | ||||
| 
 | ||||
| @ -294,7 +295,7 @@ class BugReporter @Inject constructor( | ||||
|                                     Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME | ||||
|                             ) | ||||
|                             .addFormDataPart("locale", Locale.getDefault().toString()) | ||||
|                             .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) | ||||
|                             .addFormDataPart("app_language", vectorLocale.applicationLocale.toString()) | ||||
|                             .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) | ||||
|                             .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) | ||||
|                             .addFormDataPart("server_version", serverVersion) | ||||
|  | ||||
| @ -16,10 +16,10 @@ | ||||
| 
 | ||||
| package im.vector.app.features.rageshake | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import android.os.Build | ||||
| import androidx.core.content.edit | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.core.resources.VersionCodeProvider | ||||
| import im.vector.app.features.version.VersionProvider | ||||
| import org.matrix.android.sdk.api.Matrix | ||||
| @ -31,10 +31,11 @@ import javax.inject.Singleton | ||||
| 
 | ||||
| @Singleton | ||||
| class VectorUncaughtExceptionHandler @Inject constructor( | ||||
|         context: Context, | ||||
|         @DefaultPreferences | ||||
|         private val preferences: SharedPreferences, | ||||
|         private val bugReporter: BugReporter, | ||||
|         private val versionProvider: VersionProvider, | ||||
|         private val versionCodeProvider: VersionCodeProvider | ||||
|         private val versionCodeProvider: VersionCodeProvider, | ||||
| ) : Thread.UncaughtExceptionHandler { | ||||
| 
 | ||||
|     // key to save the crash status | ||||
| @ -44,8 +45,6 @@ class VectorUncaughtExceptionHandler @Inject constructor( | ||||
| 
 | ||||
|     private var previousHandler: Thread.UncaughtExceptionHandler? = null | ||||
| 
 | ||||
|     private val preferences = DefaultSharedPreferences.getInstance(context) | ||||
| 
 | ||||
|     /** | ||||
|      * Activate this handler. | ||||
|      */ | ||||
|  | ||||
| @ -17,30 +17,40 @@ | ||||
| package im.vector.app.features.settings | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import android.content.res.Configuration | ||||
| import androidx.core.content.edit | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.core.resources.BuildMeta | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.withContext | ||||
| import timber.log.Timber | ||||
| import java.util.IllformedLocaleException | ||||
| import java.util.Locale | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| /** | ||||
|  * Object to manage the Locale choice of the user. | ||||
|  */ | ||||
| object VectorLocale { | ||||
|     private const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY" | ||||
|     private const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY" | ||||
|     private const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY" | ||||
|     private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY" | ||||
| @Singleton | ||||
| class VectorLocale @Inject constructor( | ||||
|         private val context: Context, | ||||
|         private val buildMeta: BuildMeta, | ||||
|         @DefaultPreferences | ||||
|         private val preferences: SharedPreferences, | ||||
| ) { | ||||
|     companion object { | ||||
|         const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY" | ||||
|         const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY" | ||||
|         const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY" | ||||
|         private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY" | ||||
|         private const val ISO_15924_LATN = "Latn" | ||||
|     } | ||||
| 
 | ||||
|     private val defaultLocale = Locale("en", "US") | ||||
| 
 | ||||
|     private const val ISO_15924_LATN = "Latn" | ||||
| 
 | ||||
|     /** | ||||
|      * The cache of supported application languages. | ||||
|      */ | ||||
| @ -52,17 +62,10 @@ object VectorLocale { | ||||
|     var applicationLocale = defaultLocale | ||||
|         private set | ||||
| 
 | ||||
|     private lateinit var context: Context | ||||
|     private lateinit var buildMeta: BuildMeta | ||||
| 
 | ||||
|     /** | ||||
|      * Init this object. | ||||
|      * Init this singleton. | ||||
|      */ | ||||
|     fun init(context: Context, buildMeta: BuildMeta) { | ||||
|         this.context = context | ||||
|         this.buildMeta = buildMeta | ||||
|         val preferences = DefaultSharedPreferences.getInstance(context) | ||||
| 
 | ||||
|     fun init() { | ||||
|         if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) { | ||||
|             applicationLocale = Locale( | ||||
|                     preferences.getString(APPLICATION_LOCALE_LANGUAGE_KEY, "")!!, | ||||
| @ -88,7 +91,7 @@ object VectorLocale { | ||||
|     fun saveApplicationLocale(locale: Locale) { | ||||
|         applicationLocale = locale | ||||
| 
 | ||||
|         DefaultSharedPreferences.getInstance(context).edit { | ||||
|         preferences.edit { | ||||
|             val language = locale.language | ||||
|             if (language.isEmpty()) { | ||||
|                 remove(APPLICATION_LOCALE_LANGUAGE_KEY) | ||||
|  | ||||
| @ -0,0 +1,41 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022 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.settings | ||||
| 
 | ||||
| import android.content.SharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import java.util.Locale | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * Class to provide the Locale choice of the user. | ||||
|  */ | ||||
| class VectorLocaleProvider @Inject constructor( | ||||
|         @DefaultPreferences | ||||
|         private val preferences: SharedPreferences, | ||||
| ) { | ||||
|     /** | ||||
|      * Get the current local. | ||||
|      * SharedPref values has been initialized in [VectorLocale.init] | ||||
|      */ | ||||
|     val applicationLocale: Locale | ||||
|         get() = Locale( | ||||
|                 preferences.getString(VectorLocale.APPLICATION_LOCALE_LANGUAGE_KEY, "")!!, | ||||
|                 preferences.getString(VectorLocale.APPLICATION_LOCALE_COUNTRY_KEY, "")!!, | ||||
|                 preferences.getString(VectorLocale.APPLICATION_LOCALE_VARIANT_KEY, "")!! | ||||
|         ) | ||||
| } | ||||
| @ -24,8 +24,9 @@ import androidx.annotation.BoolRes | ||||
| import androidx.core.content.edit | ||||
| import com.squareup.seismic.ShakeDetector | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.core.resources.BuildMeta | ||||
| import im.vector.app.core.resources.StringProvider | ||||
| import im.vector.app.core.time.Clock | ||||
| import im.vector.app.features.VectorFeatures | ||||
| import im.vector.app.features.disclaimer.SHARED_PREF_KEY | ||||
| @ -41,6 +42,9 @@ class VectorPreferences @Inject constructor( | ||||
|         private val clock: Clock, | ||||
|         private val buildMeta: BuildMeta, | ||||
|         private val vectorFeatures: VectorFeatures, | ||||
|         @DefaultPreferences | ||||
|         private val defaultPrefs: SharedPreferences, | ||||
|         private val stringProvider: StringProvider, | ||||
| ) { | ||||
| 
 | ||||
|     companion object { | ||||
| @ -289,8 +293,6 @@ class VectorPreferences @Inject constructor( | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private val defaultPrefs = DefaultSharedPreferences.getInstance(context) | ||||
| 
 | ||||
|     /** | ||||
|      * Allow subscribing and unsubscribing to configuration changes. This is | ||||
|      * particularly useful when you need to be notified of a configuration change | ||||
| @ -716,10 +718,10 @@ class VectorPreferences @Inject constructor( | ||||
|      */ | ||||
|     fun getSelectedMediasSavingPeriodString(): String { | ||||
|         return when (getSelectedMediasSavingPeriod()) { | ||||
|             MEDIA_SAVING_3_DAYS -> context.getString(R.string.media_saving_period_3_days) | ||||
|             MEDIA_SAVING_1_WEEK -> context.getString(R.string.media_saving_period_1_week) | ||||
|             MEDIA_SAVING_1_MONTH -> context.getString(R.string.media_saving_period_1_month) | ||||
|             MEDIA_SAVING_FOREVER -> context.getString(R.string.media_saving_period_forever) | ||||
|             MEDIA_SAVING_3_DAYS -> stringProvider.getString(R.string.media_saving_period_3_days) | ||||
|             MEDIA_SAVING_1_WEEK -> stringProvider.getString(R.string.media_saving_period_1_week) | ||||
|             MEDIA_SAVING_1_MONTH -> stringProvider.getString(R.string.media_saving_period_1_month) | ||||
|             MEDIA_SAVING_FOREVER -> stringProvider.getString(R.string.media_saving_period_forever) | ||||
|             else -> "?" | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -46,6 +46,7 @@ class VectorSettingsPreferencesFragment : | ||||
|     @Inject lateinit var vectorPreferences: VectorPreferences | ||||
|     @Inject lateinit var fontScalePreferences: FontScalePreferences | ||||
|     @Inject lateinit var vectorFeatures: VectorFeatures | ||||
|     @Inject lateinit var vectorLocale: VectorLocale | ||||
| 
 | ||||
|     override var titleRes = R.string.settings_preferences | ||||
|     override val preferenceXmlRes = R.xml.vector_settings_preferences | ||||
| @ -198,7 +199,7 @@ class VectorSettingsPreferencesFragment : | ||||
| 
 | ||||
|     private fun setUserInterfacePreferences() { | ||||
|         // Selected language | ||||
|         selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale) | ||||
|         selectedLanguagePreference.summary = vectorLocale.localeToLocalisedString(vectorLocale.applicationLocale) | ||||
| 
 | ||||
|         // Text size | ||||
|         textSizePreference.summary = getString(fontScalePreferences.getResolvedFontScaleValue().nameResId) | ||||
|  | ||||
| @ -17,7 +17,6 @@ | ||||
| 
 | ||||
| package im.vector.app.features.settings | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.Activity | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| @ -448,7 +447,6 @@ class VectorSettingsSecurityPrivacyFragment : | ||||
|     /** | ||||
|      * Manage the e2e keys import. | ||||
|      */ | ||||
|     @SuppressLint("NewApi") | ||||
|     private fun importKeys() { | ||||
|         openFileSelection( | ||||
|                 requireActivity(), | ||||
|  | ||||
| @ -23,17 +23,19 @@ import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import androidx.preference.Preference | ||||
| import androidx.preference.SwitchPreference | ||||
| import dagger.hilt.android.AndroidEntryPoint | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.extensions.registerStartForActivityResult | ||||
| import im.vector.app.core.preference.VectorPreference | ||||
| import im.vector.app.core.utils.getCallRingtoneName | ||||
| import im.vector.app.core.utils.getCallRingtoneUri | ||||
| import im.vector.app.core.utils.setCallRingtoneUri | ||||
| import im.vector.app.core.utils.setUseRiotDefaultRingtone | ||||
| import im.vector.app.core.utils.RingtoneUtils | ||||
| import im.vector.app.features.analytics.plan.MobileScreen | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| @AndroidEntryPoint | ||||
| class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() { | ||||
| 
 | ||||
|     @Inject lateinit var ringtoneUtils: RingtoneUtils | ||||
| 
 | ||||
|     override var titleRes = R.string.preference_voice_and_video | ||||
|     override val preferenceXmlRes = R.xml.vector_settings_voice_video | ||||
| 
 | ||||
| @ -52,12 +54,12 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() { | ||||
|     override fun bindPref() { | ||||
|         // Incoming call sounds | ||||
|         mUseRiotCallRingtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { | ||||
|             activity?.let { setUseRiotDefaultRingtone(it, mUseRiotCallRingtonePreference.isChecked) } | ||||
|             ringtoneUtils.setUseRiotDefaultRingtone(mUseRiotCallRingtonePreference.isChecked) | ||||
|             false | ||||
|         } | ||||
| 
 | ||||
|         mCallRingtonePreference.let { | ||||
|             activity?.let { activity -> it.summary = getCallRingtoneName(activity) } | ||||
|             it.summary = ringtoneUtils.getCallRingtoneName() | ||||
|             it.onPreferenceClickListener = Preference.OnPreferenceClickListener { | ||||
|                 displayRingtonePicker() | ||||
|                 false | ||||
| @ -68,10 +70,9 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() { | ||||
|     private val ringtoneStartForActivityResult = registerStartForActivityResult { activityResult -> | ||||
|         if (activityResult.resultCode == Activity.RESULT_OK) { | ||||
|             val callRingtoneUri: Uri? = activityResult.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) | ||||
|             val thisActivity = activity | ||||
|             if (callRingtoneUri != null && thisActivity != null) { | ||||
|                 setCallRingtoneUri(thisActivity, callRingtoneUri) | ||||
|                 mCallRingtonePreference.summary = getCallRingtoneName(thisActivity) | ||||
|             if (callRingtoneUri != null) { | ||||
|                 ringtoneUtils.setCallRingtoneUri(callRingtoneUri) | ||||
|                 mCallRingtonePreference.summary = ringtoneUtils.getCallRingtoneName() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @ -82,7 +83,7 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() { | ||||
|             putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false) | ||||
|             putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) | ||||
|             putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE) | ||||
|             activity?.let { putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, getCallRingtoneUri(it)) } | ||||
|             putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUtils.getCallRingtoneUri()) | ||||
|         } | ||||
|         ringtoneStartForActivityResult.launch(intent) | ||||
|     } | ||||
|  | ||||
| @ -37,13 +37,15 @@ import javax.inject.Inject | ||||
| class LocalePickerController @Inject constructor( | ||||
|         private val vectorPreferences: VectorPreferences, | ||||
|         private val stringProvider: StringProvider, | ||||
|         private val errorFormatter: ErrorFormatter | ||||
|         private val errorFormatter: ErrorFormatter, | ||||
|         private val vectorLocale: VectorLocale, | ||||
| ) : TypedEpoxyController<LocalePickerViewState>() { | ||||
| 
 | ||||
|     var listener: Listener? = null | ||||
| 
 | ||||
|     override fun buildModels(data: LocalePickerViewState?) { | ||||
|         val list = data?.locales ?: return | ||||
|         val currentLocale = data.currentLocale ?: return | ||||
|         val host = this | ||||
| 
 | ||||
|         profileSectionItem { | ||||
| @ -51,10 +53,10 @@ class LocalePickerController @Inject constructor( | ||||
|             title(host.stringProvider.getString(R.string.choose_locale_current_locale_title)) | ||||
|         } | ||||
|         localeItem { | ||||
|             id(data.currentLocale.toString()) | ||||
|             title(VectorLocale.localeToLocalisedString(data.currentLocale).safeCapitalize(data.currentLocale)) | ||||
|             id(currentLocale.toString()) | ||||
|             title(host.vectorLocale.localeToLocalisedString(currentLocale).safeCapitalize(currentLocale)) | ||||
|             if (host.vectorPreferences.developerMode()) { | ||||
|                 subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) | ||||
|                 subtitle(host.vectorLocale.localeToLocalisedStringInfo(currentLocale)) | ||||
|             } | ||||
|             clickListener { host.listener?.onUseCurrentClicked() } | ||||
|         } | ||||
| @ -78,13 +80,13 @@ class LocalePickerController @Inject constructor( | ||||
|                     } | ||||
|                 } else { | ||||
|                     list() | ||||
|                             .filter { it.toString() != data.currentLocale.toString() } | ||||
|                             .filter { it.toString() != currentLocale.toString() } | ||||
|                             .forEach { locale -> | ||||
|                                 localeItem { | ||||
|                                     id(locale.toString()) | ||||
|                                     title(VectorLocale.localeToLocalisedString(locale).safeCapitalize(locale)) | ||||
|                                     title(host.vectorLocale.localeToLocalisedString(locale).safeCapitalize(locale)) | ||||
|                                     if (host.vectorPreferences.developerMode()) { | ||||
|                                         subtitle(VectorLocale.localeToLocalisedStringInfo(locale)) | ||||
|                                         subtitle(host.vectorLocale.localeToLocalisedStringInfo(locale)) | ||||
|                                     } | ||||
|                                     clickListener { host.listener?.onLocaleClicked(locale) } | ||||
|                                 } | ||||
|  | ||||
| @ -30,7 +30,8 @@ import kotlinx.coroutines.launch | ||||
| 
 | ||||
| class LocalePickerViewModel @AssistedInject constructor( | ||||
|         @Assisted initialState: LocalePickerViewState, | ||||
|         private val vectorConfiguration: VectorConfiguration | ||||
|         private val vectorConfiguration: VectorConfiguration, | ||||
|         private val vectorLocale: VectorLocale, | ||||
| ) : VectorViewModel<LocalePickerViewState, LocalePickerAction, LocalePickerViewEvents>(initialState) { | ||||
| 
 | ||||
|     @AssistedFactory | ||||
| @ -39,8 +40,13 @@ class LocalePickerViewModel @AssistedInject constructor( | ||||
|     } | ||||
| 
 | ||||
|     init { | ||||
|         setState { | ||||
|             copy( | ||||
|                     currentLocale = vectorLocale.applicationLocale | ||||
|             ) | ||||
|         } | ||||
|         viewModelScope.launch { | ||||
|             val result = VectorLocale.getSupportedLocales() | ||||
|             val result = vectorLocale.getSupportedLocales() | ||||
| 
 | ||||
|             setState { | ||||
|                 copy( | ||||
| @ -59,7 +65,7 @@ class LocalePickerViewModel @AssistedInject constructor( | ||||
|     } | ||||
| 
 | ||||
|     private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) { | ||||
|         VectorLocale.saveApplicationLocale(action.locale) | ||||
|         vectorLocale.saveApplicationLocale(action.locale) | ||||
|         vectorConfiguration.applyToApplicationContext() | ||||
|         _viewEvents.post(LocalePickerViewEvents.RestartActivity) | ||||
|     } | ||||
|  | ||||
| @ -19,10 +19,9 @@ package im.vector.app.features.settings.locale | ||||
| import com.airbnb.mvrx.Async | ||||
| import com.airbnb.mvrx.MavericksState | ||||
| import com.airbnb.mvrx.Uninitialized | ||||
| import im.vector.app.features.settings.VectorLocale | ||||
| import java.util.Locale | ||||
| 
 | ||||
| data class LocalePickerViewState( | ||||
|         val currentLocale: Locale = VectorLocale.applicationLocale, | ||||
|         val currentLocale: Locale? = null, | ||||
|         val locales: Async<List<Locale>> = Uninitialized | ||||
| ) : MavericksState | ||||
|  | ||||
| @ -27,8 +27,8 @@ import androidx.annotation.ColorInt | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.content.edit | ||||
| import androidx.core.graphics.drawable.DrawableCompat | ||||
| import androidx.preference.PreferenceManager | ||||
| import im.vector.app.R | ||||
| import im.vector.app.core.di.DefaultSharedPreferences | ||||
| import timber.log.Timber | ||||
| import java.util.concurrent.atomic.AtomicReference | ||||
| 
 | ||||
| @ -84,7 +84,7 @@ object ThemeUtils { | ||||
|     fun getApplicationTheme(context: Context): String { | ||||
|         val currentTheme = this.currentTheme.get() | ||||
|         return if (currentTheme == null) { | ||||
|             val prefs = DefaultSharedPreferences.getInstance(context) | ||||
|             val prefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) | ||||
|             var themeFromPref = prefs.getString(APPLICATION_THEME_KEY, DEFAULT_THEME) ?: DEFAULT_THEME | ||||
|             if (themeFromPref == "status") { | ||||
|                 // Migrate to the default theme | ||||
|  | ||||
| @ -20,25 +20,31 @@ import android.content.Context | ||||
| import android.media.MediaCodecList | ||||
| import android.media.MediaFormat | ||||
| import android.os.Build | ||||
| import androidx.annotation.ChecksSdkIntAtLeast | ||||
| import androidx.annotation.VisibleForTesting | ||||
| import im.vector.app.features.VectorFeatures | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class VoiceRecorderProvider @Inject constructor( | ||||
|         private val context: Context, | ||||
|         private val vectorFeatures: VectorFeatures, | ||||
|         private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, | ||||
| ) { | ||||
|     fun provideVoiceRecorder(): VoiceRecorder { | ||||
|         return if (useFallbackRecorder()) { | ||||
|             VoiceRecorderL(context, Dispatchers.IO) | ||||
|         } else { | ||||
|         return if (useNativeRecorder()) { | ||||
|             VoiceRecorderQ(context) | ||||
|         } else { | ||||
|             VoiceRecorderL(context, Dispatchers.IO) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun useFallbackRecorder(): Boolean { | ||||
|         return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !hasOpusEncoder() || vectorFeatures.forceUsageOfOpusEncoder() | ||||
|     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) | ||||
|     private fun useNativeRecorder(): Boolean { | ||||
|         return buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.Q && | ||||
|                 hasOpusEncoder() && | ||||
|                 !vectorFeatures.forceUsageOfOpusEncoder() | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| package im.vector.app.features.widgets.webview | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.Activity | ||||
| import android.view.ViewGroup | ||||
| import android.webkit.CookieManager | ||||
| @ -29,7 +28,6 @@ import im.vector.app.features.themes.ThemeUtils | ||||
| import im.vector.app.features.webview.VectorWebViewClient | ||||
| import im.vector.app.features.webview.WebEventListener | ||||
| 
 | ||||
| @SuppressLint("NewApi") | ||||
| fun WebView.setupForWidget(activity: Activity, | ||||
|                            checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase, | ||||
|                            eventListener: WebEventListener, | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * Copyright (c) 2020 New Vector Ltd | ||||
|  * Copyright (c) 2022 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. | ||||
| @ -14,18 +14,12 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package im.vector.app.core.di | ||||
| package im.vector.app.features.workers.signout | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import androidx.preference.PreferenceManager | ||||
| import im.vector.app.core.platform.VectorViewModelAction | ||||
| 
 | ||||
| object DefaultSharedPreferences { | ||||
| 
 | ||||
|     @Volatile private var INSTANCE: SharedPreferences? = null | ||||
| 
 | ||||
|     fun getInstance(context: Context): SharedPreferences = | ||||
|             INSTANCE ?: synchronized(this) { | ||||
|                 INSTANCE ?: PreferenceManager.getDefaultSharedPreferences(context.applicationContext).also { INSTANCE = it } | ||||
|             } | ||||
| sealed interface ServerBackupStatusAction : VectorViewModelAction { | ||||
|     data class OnRecoverDoneForVersion(val version: String) : ServerBackupStatusAction | ||||
|     object OnBannerDisplayed : ServerBackupStatusAction | ||||
|     object OnBannerClosed : ServerBackupStatusAction | ||||
| } | ||||
| @ -16,6 +16,8 @@ | ||||
| 
 | ||||
| package im.vector.app.features.workers.signout | ||||
| 
 | ||||
| import android.content.SharedPreferences | ||||
| import androidx.core.content.edit | ||||
| import androidx.lifecycle.MutableLiveData | ||||
| import com.airbnb.mvrx.Async | ||||
| import com.airbnb.mvrx.MavericksState | ||||
| @ -24,9 +26,9 @@ import com.airbnb.mvrx.Uninitialized | ||||
| import dagger.assisted.Assisted | ||||
| import dagger.assisted.AssistedFactory | ||||
| import dagger.assisted.AssistedInject | ||||
| import im.vector.app.core.di.DefaultPreferences | ||||
| import im.vector.app.core.di.MavericksAssistedViewModelFactory | ||||
| import im.vector.app.core.di.hiltMavericksViewModelFactory | ||||
| import im.vector.app.core.platform.EmptyAction | ||||
| import im.vector.app.core.platform.EmptyViewEvents | ||||
| import im.vector.app.core.platform.VectorViewModel | ||||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||||
| @ -51,29 +53,55 @@ data class ServerBackupStatusViewState( | ||||
|  * The state representing the view. | ||||
|  * It can take one state at a time. | ||||
|  */ | ||||
| sealed class BannerState { | ||||
| sealed interface BannerState { | ||||
|     // Not yet rendered | ||||
|     object Initial : BannerState | ||||
| 
 | ||||
|     object Hidden : BannerState() | ||||
|     // View will be Gone | ||||
|     object Hidden : BannerState | ||||
| 
 | ||||
|     // Keys backup is not setup, numberOfKeys is the number of locally stored keys | ||||
|     data class Setup(val numberOfKeys: Int) : BannerState() | ||||
|     data class Setup(val numberOfKeys: Int, val doNotShowAgain: Boolean) : BannerState | ||||
| 
 | ||||
|     // Keys backup can be recovered, with version from the server | ||||
|     data class Recover(val version: String, val doNotShowForVersion: String) : BannerState | ||||
| 
 | ||||
|     // Keys backup can be updated | ||||
|     data class Update(val version: String, val doNotShowForVersion: String) : BannerState | ||||
| 
 | ||||
|     // Keys are backing up | ||||
|     object BackingUp : BannerState() | ||||
|     object BackingUp : BannerState | ||||
| } | ||||
| 
 | ||||
| class ServerBackupStatusViewModel @AssistedInject constructor( | ||||
|         @Assisted initialState: ServerBackupStatusViewState, | ||||
|         private val session: Session | ||||
|         private val session: Session, | ||||
|         @DefaultPreferences | ||||
|         private val sharedPreferences: SharedPreferences, | ||||
| ) : | ||||
|         VectorViewModel<ServerBackupStatusViewState, EmptyAction, EmptyViewEvents>(initialState), KeysBackupStateListener { | ||||
|         VectorViewModel<ServerBackupStatusViewState, ServerBackupStatusAction, EmptyViewEvents>(initialState), KeysBackupStateListener { | ||||
| 
 | ||||
|     @AssistedFactory | ||||
|     interface Factory : MavericksAssistedViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> { | ||||
|         override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel | ||||
|     } | ||||
| 
 | ||||
|     companion object : MavericksViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> by hiltMavericksViewModelFactory() | ||||
|     companion object : MavericksViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> by hiltMavericksViewModelFactory() { | ||||
|         /** | ||||
|          * Preference key for setup. Value is a boolean. | ||||
|          */ | ||||
|         private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN" | ||||
| 
 | ||||
|         /** | ||||
|          * Preference key for recover. Value is a backup version (String). | ||||
|          */ | ||||
|         private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION" | ||||
| 
 | ||||
|         /** | ||||
|          * Preference key for update. Value is a backup version (String). | ||||
|          */ | ||||
|         private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION" | ||||
|     } | ||||
| 
 | ||||
|     // Keys exported manually | ||||
|     val keysExportedToFile = MutableLiveData<Boolean>() | ||||
| @ -105,7 +133,10 @@ class ServerBackupStatusViewModel @AssistedInject constructor( | ||||
|                             pInfo.getOrNull()?.allKnown().orFalse()) | ||||
|             ) { | ||||
|                 // So 4S is not setup and we have local secrets, | ||||
|                 return@combine BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) | ||||
|                 return@combine BannerState.Setup( | ||||
|                         numberOfKeys = getNumberOfKeysToBackup(), | ||||
|                         doNotShowAgain = sharedPreferences.getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) | ||||
|                 ) | ||||
|             } | ||||
|             BannerState.Hidden | ||||
|         } | ||||
| @ -161,5 +192,47 @@ class ServerBackupStatusViewModel @AssistedInject constructor( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun handle(action: EmptyAction) {} | ||||
|     override fun handle(action: ServerBackupStatusAction) { | ||||
|         when (action) { | ||||
|             is ServerBackupStatusAction.OnRecoverDoneForVersion -> handleOnRecoverDoneForVersion(action) | ||||
|             ServerBackupStatusAction.OnBannerDisplayed -> handleOnBannerDisplayed() | ||||
|             ServerBackupStatusAction.OnBannerClosed -> handleOnBannerClosed() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun handleOnRecoverDoneForVersion(action: ServerBackupStatusAction.OnRecoverDoneForVersion) { | ||||
|         sharedPreferences.edit { | ||||
|             putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, action.version) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun handleOnBannerDisplayed() { | ||||
|         sharedPreferences.edit { | ||||
|             putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) | ||||
|             putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun handleOnBannerClosed() = withState { state -> | ||||
|         when (val bannerState = state.bannerState()) { | ||||
|             is BannerState.Setup -> { | ||||
|                 sharedPreferences.edit { | ||||
|                     putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true) | ||||
|                 } | ||||
|             } | ||||
|             is BannerState.Recover -> { | ||||
|                 sharedPreferences.edit { | ||||
|                     putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, bannerState.version) | ||||
|                 } | ||||
|             } | ||||
|             is BannerState.Update -> { | ||||
|                 sharedPreferences.edit { | ||||
|                     putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, bannerState.version) | ||||
|                 } | ||||
|             } | ||||
|             else -> { | ||||
|                 // Should not happen, close button is not displayed in other cases | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -34,6 +34,7 @@ | ||||
|         android:icon="@drawable/ic_filter" | ||||
|         android:title="@string/home_filter_placeholder_home" | ||||
|         app:iconTint="?vctr_content_secondary" | ||||
|         app:showAsAction="always" /> | ||||
|         app:showAsAction="always" | ||||
|         tools:ignore="AlwaysShowAction" /> | ||||
| 
 | ||||
| </menu> | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools"> | ||||
| 
 | ||||
|     <item | ||||
|         android:id="@+id/action_delete" | ||||
|         android:icon="@drawable/ic_delete_unsent_messages" | ||||
|         android:title="@string/action_delete" | ||||
|         app:showAsAction="always" /> | ||||
|         app:showAsAction="always" | ||||
|         tools:ignore="AlwaysShowAction" /> | ||||
| 
 | ||||
|     <item | ||||
|         android:id="@+id/action_mark_as_suggested" | ||||
| @ -18,4 +20,4 @@ | ||||
|         android:title="@string/space_mark_as_not_suggested" | ||||
|         app:showAsAction="never" /> | ||||
| 
 | ||||
| </menu> | ||||
| </menu> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user