mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
68907ff6c0
37
CHANGES.md
37
CHANGES.md
@ -1,9 +1,33 @@
|
||||
Changes in Element 1.0.8 (2020-XX-XX)
|
||||
Changes in Element 1.0.9 (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
|
||||
Improvements 🙌:
|
||||
- PIN code: request PIN code if phone has been locked
|
||||
- Small optimisation of scrolling experience in timeline (#2114)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Improve support for image selection with intent changes (#1376)
|
||||
- Fix Splash layout on small screens
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
|
||||
Build 🧱:
|
||||
- Use Update Gradle Wrapper Action
|
||||
|
||||
Other changes:
|
||||
- Added registration/verification automated UI tests
|
||||
- Create a script to help getting public information form any homeserver
|
||||
|
||||
Changes in Element 1.0.8 (2020-09-25)
|
||||
===================================================
|
||||
|
||||
Improvements 🙌:
|
||||
- Add "show password" in import Megolm keys dialog
|
||||
- Visually disable call buttons in menu and prohibit calling when permissions are insufficient (#2112)
|
||||
@ -12,23 +36,20 @@ Improvements 🙌:
|
||||
- Use cache for user color
|
||||
- Allow using an outdated homeserver, at user's risk (#1972)
|
||||
- Restore small logo on login screens and fix scrolling issue on those screens
|
||||
- PIN Code Improvements: Add more settings: biometrics, grace period, notification content (#1985)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Long message cannot be sent/takes infinite time & blocks other messages #1397
|
||||
- Long message cannot be sent/takes infinite time & blocks other messages (#1397)
|
||||
- Fix crash when wellknown are malformed, or redirect to some HTML content (reported by rageshakes)
|
||||
- User Verification in DM not working
|
||||
- Manual import of Megolm keys does back up the imported keys
|
||||
- Auto scrolling to the latest message when sending (#2094)
|
||||
- Fix incorrect permission check when creating widgets (#2137)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
- Pin code: user has to enter pin code twice (#2005)
|
||||
|
||||
SDK API changes ⚠️:
|
||||
- Rename `tryThis` to `tryOrNull`
|
||||
|
||||
Build 🧱:
|
||||
- Update Gradle Wrapper Action
|
||||
|
||||
Other changes:
|
||||
- Add an advanced action to reset an account data entry
|
||||
|
||||
|
107
docs/ui-tests.md
Normal file
107
docs/ui-tests.md
Normal file
@ -0,0 +1,107 @@
|
||||
# Automate user interface tests
|
||||
|
||||
Element Android ensures that some fundamental flows are properly working by running automated user interface tests.
|
||||
Ui tests are using the android [Espresso](https://developer.android.com/training/testing/espresso) library.
|
||||
|
||||
Tests can be run on a real device, or on a virtual device (such as the emulator in Android Studio).
|
||||
|
||||
Currently the test are covering a small set of application flows:
|
||||
- Registration
|
||||
- Self verification via emoji
|
||||
- Self verification via passphrase
|
||||
|
||||
## Prerequisites:
|
||||
|
||||
Out of the box, the tests use one of the homeservers (located at http://localhost:8080) of the "Demo Federation of Homeservers" (https://github.com/matrix-org/synapse#running-a-demo-federation-of-synapses).
|
||||
|
||||
You first need to follow instructions to set up Synapse in development mode at https://github.com/matrix-org/synapse#synapse-development. If you have already installed all dependencies, the steps are:
|
||||
|
||||
```shell script
|
||||
$ git clone https://github.com/matrix-org/synapse.git
|
||||
$ cd synapse
|
||||
$ virtualenv -p python3 env
|
||||
$ source env/bin/activate
|
||||
(env) $ python -m pip install --no-use-pep517 -e .
|
||||
```
|
||||
|
||||
Every time you want to launch these test homeservers, type:
|
||||
|
||||
```shell script
|
||||
$ virtualenv -p python3 env
|
||||
$ source env/bin/activate
|
||||
(env) $ demo/start.sh --no-rate-limit
|
||||
```
|
||||
|
||||
**Emulator/Device set up**
|
||||
|
||||
When running the test via android studio on a device, you have to disable system animations in order for the test to work properly.
|
||||
|
||||
First, ensure developer mode is enabled:
|
||||
|
||||
- To enable developer options, tap the **Build Number** option 7 times. You can find this option in one of the following locations, depending on your Android version:
|
||||
|
||||
- Android 9 (API level 28) and higher: **Settings > About Phone > Build Number**
|
||||
- Android 8.0.0 (API level 26) and Android 8.1.0 (API level 26): **Settings > System > About Phone > Build Number**
|
||||
- Android 7.1 (API level 25) and lower: **Settings > About Phone > Build Number**
|
||||
|
||||
On your device, under **Settings > Developer options**, disable the following 3 settings:
|
||||
|
||||
- Window animation scale
|
||||
- Transition animation scale
|
||||
- Animator duration scale
|
||||
|
||||
## Run the tests
|
||||
|
||||
Once Synapse is running, and an emulator is running, you can run the UI tests.
|
||||
|
||||
### From the source code
|
||||
|
||||
Click on the green arrow in front of each test. Clicking on the arrow in front of the test class, or from the package directory does not always work (Tests not found issue).
|
||||
|
||||
### From command line
|
||||
|
||||
````shell script
|
||||
./gradlew vector:connectedGplayDebugAndroidTest
|
||||
````
|
||||
|
||||
To run all the tests from the `vector` module.
|
||||
|
||||
In case of trouble, you can try to uninstall the previous installed test APK first with this command:
|
||||
|
||||
```shell script
|
||||
adb uninstall im.vector.app.debug.test
|
||||
```
|
||||
## Recipes
|
||||
|
||||
We added some specific Espresso IdlingResources, and other utilities for matrix related tests
|
||||
|
||||
### Wait for initial sync
|
||||
|
||||
```kotlin
|
||||
// Wait for initial sync and check room list is there
|
||||
withIdlingResource(initialSyncIdlingResource(uiSession)) {
|
||||
onView(withId(R.id.roomListContainer))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing current activity
|
||||
|
||||
```kotlin
|
||||
val activity = EspressoHelper.getCurrentActivity()!!
|
||||
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
|
||||
```
|
||||
|
||||
### Interact with other session
|
||||
|
||||
It's possible to create a session via the SDK, and then use this session to interact with the one that the emulator is using (to check verifications for example)
|
||||
|
||||
```kotlin
|
||||
@Before
|
||||
fun initAccount() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val matrix = Matrix.getInstance(context)
|
||||
val userName = "foobar_${System.currentTimeMillis()}"
|
||||
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||
}
|
||||
```
|
@ -218,7 +218,7 @@ class CommonTestHelper(context: Context) {
|
||||
.createAccount(userName, password, null, it)
|
||||
}
|
||||
|
||||
// Preform dummy step
|
||||
// Perform dummy step
|
||||
val registrationResult = doSync<RegistrationResult> {
|
||||
matrix.authenticationService
|
||||
.getRegistrationWizard()
|
||||
|
@ -126,7 +126,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
|
||||
Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
|
||||
Timber.v("## CRYPTO - GOSSIP sendOutgoingGossipingRequest() : Requesting keys $request")
|
||||
|
||||
val params = SendGossipRequestWorker.Params(
|
||||
sessionId = sessionId,
|
||||
|
@ -372,6 +372,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
|
||||
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||
xSignMasterPrivateKey = msk
|
||||
@ -407,6 +408,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun storeMSKPrivateKey(msk: String?) {
|
||||
Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ")
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||
xSignMasterPrivateKey = msk
|
||||
@ -415,6 +417,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun storeSSKPrivateKey(ssk: String?) {
|
||||
Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ")
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||
xSignSelfSignedPrivateKey = ssk
|
||||
@ -423,6 +426,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
}
|
||||
|
||||
override fun storeUSKPrivateKey(usk: String?) {
|
||||
Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ")
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||
xSignUserPrivateKey = usk
|
||||
|
@ -82,7 +82,7 @@ class ImagePicker(override val requestCode: Int) : Picker<MultiPickerImageType>(
|
||||
}
|
||||
|
||||
override fun createIntent(): Intent {
|
||||
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
return Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||
type = "image/*"
|
||||
|
69
tools/hs_diag.py
Executable file
69
tools/hs_diag.py
Executable file
@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2020 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.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
### Arguments
|
||||
|
||||
parser = argparse.ArgumentParser(description='Get some information about a homeserver.')
|
||||
parser.add_argument('-s',
|
||||
'--homeserver',
|
||||
required=True,
|
||||
help="homeserver URL")
|
||||
parser.add_argument('-v',
|
||||
'--verbose',
|
||||
help="increase output verbosity.",
|
||||
action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
print("Argument:")
|
||||
print(args)
|
||||
|
||||
baseUrl = args.homeserver
|
||||
|
||||
if not baseUrl.startswith("http"):
|
||||
baseUrl = "https://" + baseUrl
|
||||
|
||||
if not baseUrl.endswith("/"):
|
||||
baseUrl = baseUrl + "/"
|
||||
|
||||
print("Get information from " + baseUrl)
|
||||
|
||||
items = [
|
||||
# [Title, URL, True for GET request and False for POST request]
|
||||
["Well-known", baseUrl + ".well-known/matrix/client", True]
|
||||
, ["Version", baseUrl + "_matrix/client/versions", True]
|
||||
, ["Login flow", baseUrl + "_matrix/client/r0/login", True]
|
||||
, ["Registration flow", baseUrl + "_matrix/client/r0/register", False]
|
||||
# Useless , ["Username availability", baseUrl + "_matrix/client/r0/register/available?username=benoit", True]
|
||||
# Useless , ["Public rooms", baseUrl + "_matrix/client/r0/publicRooms?limit=1", True]
|
||||
# Useless , ["Profile", baseUrl + "_matrix/client/r0/profile/@benoit.marty:matrix.org", True]
|
||||
# Need token , ["Capability", baseUrl + "_matrix/client/r0/capabilities", True]
|
||||
# Need token , ["Media config", baseUrl + "_matrix/media/r0/config", True]
|
||||
# Need token , ["Turn", baseUrl + "_matrix/client/r0/voip/turnServer", True]
|
||||
]
|
||||
|
||||
for item in items:
|
||||
print("====================================================================================================")
|
||||
print("# " + item[0] + " (" + item[1] + ")")
|
||||
print("====================================================================================================")
|
||||
if item[2]:
|
||||
os.system("curl -s -X GET '" + item[1] + "' | python -m json.tool")
|
||||
else:
|
||||
os.system("curl -s -X POST --data $'{}' '" + item[1] + "' | python -m json.tool")
|
@ -17,7 +17,7 @@ androidExtensions {
|
||||
// Note: 2 digits max for each value
|
||||
ext.versionMajor = 1
|
||||
ext.versionMinor = 0
|
||||
ext.versionPatch = 8
|
||||
ext.versionPatch = 9
|
||||
|
||||
static def getGitTimestamp() {
|
||||
def cmd = 'git show -s --format=%ct'
|
||||
@ -172,6 +172,19 @@ android {
|
||||
output.versionCodeOverride = variant.versionCode * 10 + baseAbiVersionCode
|
||||
}
|
||||
}
|
||||
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
// Disables animations during instrumented tests you run from the command line…
|
||||
// This property does not affect tests that you run using Android Studio.”
|
||||
animationsDisabled = true
|
||||
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@ -281,6 +294,11 @@ dependencies {
|
||||
def arch_version = '2.1.0'
|
||||
def lifecycle_version = '2.2.0'
|
||||
|
||||
// Tests
|
||||
def kluent_version = '1.44'
|
||||
def androidxTest_version = '1.3.0'
|
||||
def espresso_version = '3.3.0'
|
||||
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation project(":matrix-sdk-android-rx")
|
||||
implementation project(":diff-match-patch")
|
||||
@ -326,6 +344,7 @@ dependencies {
|
||||
implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0'
|
||||
|
||||
implementation("com.airbnb.android:epoxy:$epoxy_version")
|
||||
implementation "com.airbnb.android:epoxy-glide-preloading:$epoxy_version"
|
||||
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
|
||||
implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
|
||||
implementation 'com.airbnb.android:mvrx:1.3.0'
|
||||
@ -352,7 +371,7 @@ dependencies {
|
||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||
implementation 'com.google.android:flexbox:1.1.1'
|
||||
implementation "androidx.autofill:autofill:$autofill_version"
|
||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta9'
|
||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10'
|
||||
|
||||
// Custom Tab
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
@ -421,19 +440,20 @@ dependencies {
|
||||
|
||||
// TESTS
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||
// Plant Timber tree for test
|
||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
// Activate when you want to check for leaks, from time to time.
|
||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
androidTestImplementation "androidx.test:core:$androidxTest_version"
|
||||
androidTestImplementation "androidx.test:runner:$androidxTest_version"
|
||||
androidTestImplementation "androidx.test:rules:$androidxTest_version"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
|
||||
androidTestImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
|
||||
// Plant Timber tree for test
|
||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
207
vector/src/androidTest/java/im/vector/app/EspressoExt.kt
Normal file
207
vector/src/androidTest/java/im/vector/app/EspressoExt.kt
Normal file
@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.app.Activity
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.IdlingResource
|
||||
import androidx.test.espresso.PerformException
|
||||
import androidx.test.espresso.UiController
|
||||
import androidx.test.espresso.ViewAction
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.util.HumanReadables
|
||||
import androidx.test.espresso.util.TreeIterables
|
||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.test.runner.lifecycle.ActivityLifecycleCallback
|
||||
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
|
||||
import androidx.test.runner.lifecycle.Stage
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.StringDescription
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
object EspressoHelper {
|
||||
fun getCurrentActivity(): Activity? {
|
||||
var currentActivity: Activity? = null
|
||||
getInstrumentation().runOnMainSync {
|
||||
currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
|
||||
}
|
||||
return currentActivity
|
||||
}
|
||||
}
|
||||
|
||||
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View> {
|
||||
return Matchers.any(View::class.java)
|
||||
}
|
||||
|
||||
override fun getDescription(): String {
|
||||
val matcherDescription = StringDescription()
|
||||
viewMatcher.describeTo(matcherDescription)
|
||||
return "wait for a specific view <$matcherDescription> to be ${if (waitForDisplayed) "displayed" else "not displayed during $timeout millis."}"
|
||||
}
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
println("*** waitForView 1 $view")
|
||||
uiController.loopMainThreadUntilIdle()
|
||||
val startTime = System.currentTimeMillis()
|
||||
val endTime = startTime + timeout
|
||||
val visibleMatcher = isDisplayed()
|
||||
|
||||
do {
|
||||
println("*** waitForView loop $view end:$endTime current:${System.currentTimeMillis()}")
|
||||
val viewVisible = TreeIterables.breadthFirstViewTraversal(view)
|
||||
.any { viewMatcher.matches(it) && visibleMatcher.matches(it) }
|
||||
|
||||
println("*** waitForView loop viewVisible:$viewVisible")
|
||||
if (viewVisible == waitForDisplayed) return
|
||||
println("*** waitForView loop loopMainThreadForAtLeast...")
|
||||
uiController.loopMainThreadForAtLeast(50)
|
||||
println("*** waitForView loop ...loopMainThreadForAtLeast")
|
||||
} while (System.currentTimeMillis() < endTime)
|
||||
|
||||
println("*** waitForView timeout $view")
|
||||
// Timeout happens.
|
||||
throw PerformException.Builder()
|
||||
.withActionDescription(this.description)
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.withCause(TimeoutException())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initialSyncIdlingResource(session: Session): IdlingResource {
|
||||
val res = object : IdlingResource, Observer<SyncState> {
|
||||
private var callback: IdlingResource.ResourceCallback? = null
|
||||
|
||||
override fun getName() = "InitialSyncIdlingResource for ${session.myUserId}"
|
||||
|
||||
override fun isIdleNow(): Boolean {
|
||||
val isIdle = session.hasAlreadySynced()
|
||||
return isIdle
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||
this.callback = callback
|
||||
}
|
||||
|
||||
override fun onChanged(t: SyncState?) {
|
||||
val isIdle = session.hasAlreadySynced()
|
||||
if (isIdle) {
|
||||
callback?.onTransitionToIdle()
|
||||
session.getSyncStateLive().removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
session.getSyncStateLive().observeForever(res)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
fun activityIdlingResource(activityClass: Class<*>): IdlingResource {
|
||||
val res = object : IdlingResource, ActivityLifecycleCallback {
|
||||
private var callback: IdlingResource.ResourceCallback? = null
|
||||
|
||||
var hasResumed = false
|
||||
private var currentActivity : Activity? = null
|
||||
|
||||
val uniqTS = System.currentTimeMillis()
|
||||
override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqTS"
|
||||
|
||||
override fun isIdleNow(): Boolean {
|
||||
val currentActivity = currentActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
|
||||
|
||||
val isIdle = hasResumed || currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false
|
||||
println("*** [$name] isIdleNow activityIdlingResource $currentActivity isIdle:$isIdle")
|
||||
return isIdle
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||
println("*** [$name] registerIdleTransitionCallback $callback")
|
||||
this.callback = callback
|
||||
// if (hasResumed) callback?.onTransitionToIdle()
|
||||
}
|
||||
|
||||
override fun onActivityLifecycleChanged(activity: Activity?, stage: Stage?) {
|
||||
println("*** [$name] onActivityLifecycleChanged $activity $stage")
|
||||
currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
|
||||
val isIdle = currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false
|
||||
println("*** [$name] onActivityLifecycleChanged $currentActivity isIdle:$isIdle")
|
||||
if (isIdle) {
|
||||
hasResumed = true
|
||||
println("*** [$name] onActivityLifecycleChanged callback: $callback")
|
||||
callback?.onTransitionToIdle()
|
||||
ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(res)
|
||||
return res
|
||||
}
|
||||
|
||||
fun withIdlingResource(idlingResource: IdlingResource, block: (() -> Unit)) {
|
||||
println("*** withIdlingResource register")
|
||||
IdlingRegistry.getInstance().register(idlingResource)
|
||||
block.invoke()
|
||||
println("*** withIdlingResource unregister")
|
||||
IdlingRegistry.getInstance().unregister(idlingResource)
|
||||
}
|
||||
|
||||
fun allSecretsKnownIdling(session: Session): IdlingResource {
|
||||
val res = object : IdlingResource, Observer<Optional<PrivateKeysInfo>> {
|
||||
private var callback: IdlingResource.ResourceCallback? = null
|
||||
|
||||
var privateKeysInfo: PrivateKeysInfo? = session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()
|
||||
override fun getName() = "AllSecretsKnownIdling_${session.myUserId}"
|
||||
|
||||
override fun isIdleNow(): Boolean {
|
||||
println("*** [$name]/isIdleNow allSecretsKnownIdling ${privateKeysInfo?.allKnown()}")
|
||||
return privateKeysInfo?.allKnown() == true
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||
this.callback = callback
|
||||
}
|
||||
|
||||
override fun onChanged(t: Optional<PrivateKeysInfo>?) {
|
||||
println("*** [$name] allSecretsKnownIdling ${t?.getOrNull()}")
|
||||
privateKeysInfo = t?.getOrNull()
|
||||
if (t?.getOrNull()?.allKnown() == true) {
|
||||
session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().removeObserver(this)
|
||||
callback?.onTransitionToIdle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().observeForever(res)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
120
vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
Normal file
120
vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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 androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.home.HomeActivity
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class RegistrationTest {
|
||||
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun simpleRegister() {
|
||||
val userId: String = "UiAutoTest_${System.currentTimeMillis()}"
|
||||
val password: String = "password"
|
||||
val homeServerUrl: String = "http://10.0.2.2:8080"
|
||||
|
||||
// Check splashscreen is there
|
||||
onView(withId(R.id.loginSplashSubmit))
|
||||
.check(matches(isDisplayed()))
|
||||
.check(matches(withText(R.string.login_splash_submit)))
|
||||
|
||||
// Click on get started
|
||||
onView(withId(R.id.loginSplashSubmit))
|
||||
.perform(click())
|
||||
|
||||
// Check that home server options are shown
|
||||
onView(withId(R.id.loginServerTitle))
|
||||
.check(matches(isDisplayed()))
|
||||
.check(matches(withText(R.string.login_server_title)))
|
||||
|
||||
// Chose custom server
|
||||
onView(withId(R.id.loginServerChoiceOther))
|
||||
.perform(click())
|
||||
|
||||
// Enter local synapse
|
||||
onView((withId(R.id.loginServerUrlFormHomeServerUrl)))
|
||||
.perform(typeText(homeServerUrl))
|
||||
|
||||
// Click on continue
|
||||
onView(withId(R.id.loginServerUrlFormSubmit))
|
||||
.check(matches(isEnabled()))
|
||||
.perform(closeSoftKeyboard(), click())
|
||||
|
||||
// Click on the signup button
|
||||
onView(withId(R.id.loginSignupSigninSubmit))
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
|
||||
// Ensure password flow supported
|
||||
onView(withId(R.id.loginField))
|
||||
.check(matches(isDisplayed()))
|
||||
onView(withId(R.id.passwordField))
|
||||
.check(matches(isDisplayed()))
|
||||
|
||||
// Ensure user id
|
||||
onView((withId(R.id.loginField)))
|
||||
.perform(typeText(userId))
|
||||
|
||||
// Ensure login button not yet enabled
|
||||
onView(withId(R.id.loginSubmit))
|
||||
.check(matches(not(isEnabled())))
|
||||
|
||||
// Ensure password
|
||||
onView((withId(R.id.passwordField)))
|
||||
.perform(closeSoftKeyboard(), typeText(password))
|
||||
|
||||
// Submit
|
||||
onView(withId(R.id.loginSubmit))
|
||||
.check(matches(isEnabled()))
|
||||
.perform(closeSoftKeyboard(), click())
|
||||
|
||||
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||
onView(withId(R.id.roomListContainer))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
val activity = EspressoHelper.getCurrentActivity()!!
|
||||
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
|
||||
|
||||
// Wait for initial sync and check room list is there
|
||||
withIdlingResource(initialSyncIdlingResource(uiSession)) {
|
||||
onView(withId(R.id.roomListContainer))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.view.View;
|
||||
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
|
||||
public class SleepViewAction {
|
||||
|
||||
public static ViewAction sleep(final long millis) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Wait for at least " + millis + " millis";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(final UiController uiController, final View view) {
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
uiController.loopMainThreadForAtLeast(millis);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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 androidx.annotation.CallSuper
|
||||
import junit.framework.TestCase.fail
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
/**
|
||||
* Simple implementation of MatrixCallback, which count down the CountDownLatch on each API callback
|
||||
* @param onlySuccessful true to fail if an error occurs. This is the default behavior
|
||||
* @param <T>
|
||||
*/
|
||||
open class TestMatrixCallback<T>(private val countDownLatch: CountDownLatch,
|
||||
private val onlySuccessful: Boolean = true) : MatrixCallback<T> {
|
||||
|
||||
@CallSuper
|
||||
override fun onSuccess(data: T) {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e(failure, "TestApiCallback")
|
||||
|
||||
if (onlySuccessful) {
|
||||
fail("onFailure " + failure.localizedMessage)
|
||||
}
|
||||
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.net.Uri
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.assertion.ViewAssertions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.junit.Assert
|
||||
import org.matrix.android.sdk.api.Matrix
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class VerificationTestBase {
|
||||
|
||||
val password = "password"
|
||||
val homeServerUrl: String = "http://10.0.2.2:8080"
|
||||
|
||||
fun doLogin(homeServerUrl: String, userId: String, password: String) {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
|
||||
.perform(ViewActions.click())
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
|
||||
|
||||
// Chose custom server
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
|
||||
.perform(ViewActions.click())
|
||||
|
||||
// Enter local synapse
|
||||
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
|
||||
.perform(ViewActions.typeText(homeServerUrl))
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
|
||||
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
|
||||
|
||||
// Click on the signin button
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSignIn))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
.perform(ViewActions.click())
|
||||
|
||||
// Ensure password flow supported
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginField))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
|
||||
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
|
||||
.perform(ViewActions.typeText(userId))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
|
||||
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
|
||||
|
||||
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
|
||||
.perform(ViewActions.closeSoftKeyboard(), ViewActions.typeText(password))
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
|
||||
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
|
||||
}
|
||||
|
||||
private fun createAccount(userId: String = "UiAutoTest", password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
|
||||
.perform(ViewActions.click())
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
|
||||
|
||||
// Chose custom server
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
|
||||
.perform(ViewActions.click())
|
||||
|
||||
// Enter local synapse
|
||||
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
|
||||
.perform(ViewActions.typeText(homeServerUrl))
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
|
||||
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
|
||||
|
||||
// Click on the signup button
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSubmit))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
.perform(ViewActions.click())
|
||||
|
||||
// Ensure password flow supported
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginField))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
|
||||
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
|
||||
.perform(ViewActions.typeText(userId))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
|
||||
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
|
||||
|
||||
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
|
||||
.perform(ViewActions.typeText(password))
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
|
||||
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.homeDrawerFragmentContainer))
|
||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||
}
|
||||
|
||||
fun createAccountAndSync(matrix: Matrix, userName: String,
|
||||
password: String,
|
||||
withInitialSync: Boolean): Session {
|
||||
val hs = createHomeServerConfig()
|
||||
|
||||
doSync<LoginFlowResult> {
|
||||
matrix.authenticationService()
|
||||
.getLoginFlow(hs, it)
|
||||
}
|
||||
|
||||
doSync<RegistrationResult> {
|
||||
matrix.authenticationService()
|
||||
.getRegistrationWizard()
|
||||
.createAccount(userName, password, null, it)
|
||||
}
|
||||
|
||||
// Perform dummy step
|
||||
val registrationResult = doSync<RegistrationResult> {
|
||||
matrix.authenticationService()
|
||||
.getRegistrationWizard()
|
||||
.dummy(it)
|
||||
}
|
||||
|
||||
Assert.assertTrue(registrationResult is RegistrationResult.Success)
|
||||
val session = (registrationResult as RegistrationResult.Success).session
|
||||
if (withInitialSync) {
|
||||
syncSession(session)
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
fun createHomeServerConfig(): HomeServerConnectionConfig {
|
||||
return HomeServerConnectionConfig.Builder()
|
||||
.withHomeServerUri(Uri.parse(homeServerUrl))
|
||||
.build()
|
||||
}
|
||||
|
||||
// Transform a method with a MatrixCallback to a synchronous method
|
||||
inline fun <reified T> doSync(block: (MatrixCallback<T>) -> Unit): T {
|
||||
val lock = CountDownLatch(1)
|
||||
var result: T? = null
|
||||
|
||||
val callback = object : TestMatrixCallback<T>(lock) {
|
||||
override fun onSuccess(data: T) {
|
||||
result = data
|
||||
super.onSuccess(data)
|
||||
}
|
||||
}
|
||||
|
||||
block.invoke(callback)
|
||||
|
||||
lock.await(20_000, TimeUnit.MILLISECONDS)
|
||||
|
||||
Assert.assertNotNull(result)
|
||||
return result!!
|
||||
}
|
||||
|
||||
fun syncSession(session: Session) {
|
||||
val lock = CountDownLatch(1)
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) { session.open() }
|
||||
|
||||
session.startSync(true)
|
||||
|
||||
val syncLiveData = runBlocking(Dispatchers.Main) {
|
||||
session.getSyncStateLive()
|
||||
}
|
||||
val syncObserver = object : Observer<SyncState> {
|
||||
override fun onChanged(t: SyncState?) {
|
||||
if (session.hasAlreadySynced()) {
|
||||
lock.countDown()
|
||||
syncLiveData.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) }
|
||||
|
||||
lock.await(20_000, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.IdlingResource
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.home.HomeActivity
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.matrix.android.sdk.api.Matrix
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class VerifySessionInteractiveTest : VerificationTestBase() {
|
||||
|
||||
var existingSession: Session? = null
|
||||
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||
|
||||
@Before
|
||||
fun createSessionWithCrossSigning() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val matrix = Matrix.getInstance(context)
|
||||
val userName = "foobar_${System.currentTimeMillis()}"
|
||||
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||
doSync<Unit> {
|
||||
existingSession!!.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(UserPasswordAuth(
|
||||
user = existingSession!!.myUserId,
|
||||
password = "password"
|
||||
), it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkVerifyPopup() {
|
||||
val userId: String = existingSession!!.myUserId
|
||||
|
||||
doLogin(homeServerUrl, userId, password)
|
||||
|
||||
// Thread.sleep(6000)
|
||||
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||
onView(withId(R.id.roomListContainer))
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(closeSoftKeyboard())
|
||||
}
|
||||
|
||||
val activity = EspressoHelper.getCurrentActivity()!!
|
||||
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
|
||||
|
||||
withIdlingResource(initialSyncIdlingResource(uiSession)) {
|
||||
onView(withId(R.id.roomListContainer))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
|
||||
// Cannot wait for view because of alerter animation? ...
|
||||
onView(isRoot())
|
||||
.perform(waitForView(withId(com.tapadoo.alerter.R.id.llAlertBackground)))
|
||||
// Thread.sleep(1000)
|
||||
// onView(withId(com.tapadoo.alerter.R.id.llAlertBackground))
|
||||
// .perform(click())
|
||||
Thread.sleep(1000)
|
||||
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
|
||||
activity.runOnUiThread {
|
||||
popup.performClick()
|
||||
}
|
||||
|
||||
onView(isRoot())
|
||||
.perform(waitForView(withId(R.id.bottomSheetFragmentContainer)))
|
||||
// .check()
|
||||
// onView(withId(R.id.bottomSheetFragmentContainer))
|
||||
// .check(matches(isDisplayed()))
|
||||
|
||||
// onView(isRoot()).perform(SleepViewAction.sleep(2000))
|
||||
|
||||
onView(withText(R.string.use_latest_app))
|
||||
.check(matches(isDisplayed()))
|
||||
|
||||
// 4S is not setup so passphrase option should be hidden
|
||||
onView(withId(R.id.bottomSheetFragmentContainer))
|
||||
.check(matches(not(hasDescendant(withText(R.string.verification_cannot_access_other_session)))))
|
||||
|
||||
val request = existingSession!!.cryptoService().verificationService().requestKeyVerification(
|
||||
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||
existingSession!!.myUserId,
|
||||
listOf(uiSession.sessionParams.deviceId!!)
|
||||
)
|
||||
|
||||
val transactionId = request.transactionId!!
|
||||
val sasReadyIdle = verificationStateIdleResource(transactionId, VerificationTxState.ShortCodeReady, uiSession)
|
||||
val otherSessionSasReadyIdle = verificationStateIdleResource(transactionId, VerificationTxState.ShortCodeReady, existingSession!!)
|
||||
|
||||
onView(isRoot()).perform(SleepViewAction.sleep(1000))
|
||||
|
||||
// Assert QR code option is there and available
|
||||
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||
.check(matches(hasDescendant(withText(R.string.verification_scan_their_code))))
|
||||
|
||||
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||
.check(matches(hasDescendant(withId(R.id.itemVerificationQrCodeImage))))
|
||||
|
||||
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||
.perform(
|
||||
actionOnItem<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText(R.string.verification_scan_emoji_title)),
|
||||
click()
|
||||
)
|
||||
)
|
||||
|
||||
val firstSessionTr = existingSession!!.cryptoService().verificationService().getExistingTransaction(
|
||||
existingSession!!.myUserId,
|
||||
transactionId
|
||||
) as SasVerificationTransaction
|
||||
|
||||
IdlingRegistry.getInstance().register(sasReadyIdle)
|
||||
IdlingRegistry.getInstance().register(otherSessionSasReadyIdle)
|
||||
onView(isRoot()).perform(SleepViewAction.sleep(300))
|
||||
// will only execute when Idle is ready
|
||||
val expectedEmojis = firstSessionTr.getEmojiCodeRepresentation()
|
||||
val targets = listOf(R.id.emoji0, R.id.emoji1, R.id.emoji2, R.id.emoji3, R.id.emoji4, R.id.emoji5, R.id.emoji6)
|
||||
targets.forEachIndexed { index, res ->
|
||||
onView(withId(res))
|
||||
.check(
|
||||
matches(hasDescendant(withText(expectedEmojis[index].nameResId)))
|
||||
)
|
||||
}
|
||||
|
||||
IdlingRegistry.getInstance().unregister(sasReadyIdle)
|
||||
IdlingRegistry.getInstance().unregister(otherSessionSasReadyIdle)
|
||||
|
||||
val verificationSuccessIdle =
|
||||
verificationStateIdleResource(transactionId, VerificationTxState.Verified, uiSession)
|
||||
|
||||
// CLICK ON THEY MATCH
|
||||
|
||||
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||
.perform(
|
||||
actionOnItem<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText(R.string.verification_sas_match)),
|
||||
click()
|
||||
)
|
||||
)
|
||||
|
||||
firstSessionTr.userHasVerifiedShortCode()
|
||||
|
||||
onView(isRoot()).perform(SleepViewAction.sleep(1000))
|
||||
|
||||
withIdlingResource(verificationSuccessIdle) {
|
||||
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||
.check(
|
||||
matches(hasDescendant(withText(R.string.verification_conclusion_ok_self_notice)))
|
||||
)
|
||||
}
|
||||
|
||||
// Wait a bit before done (to delay a bit sending of secrets to let other have time
|
||||
// to mark as verified :/
|
||||
Thread.sleep(5_000)
|
||||
// Click on done
|
||||
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||
.perform(
|
||||
actionOnItem<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText(R.string.done)),
|
||||
click()
|
||||
)
|
||||
)
|
||||
|
||||
// Wait until local secrets are known (gossip)
|
||||
withIdlingResource(allSecretsKnownIdling(uiSession)) {
|
||||
onView(withId(R.id.groupToolbarAvatarImageView))
|
||||
.perform(click())
|
||||
}
|
||||
}
|
||||
|
||||
fun signout() {
|
||||
onView((withId(R.id.groupToolbarAvatarImageView)))
|
||||
.perform(click())
|
||||
|
||||
onView((withId(R.id.homeDrawerHeaderSettingsView)))
|
||||
.perform(click())
|
||||
|
||||
onView(withText("General"))
|
||||
.perform(click())
|
||||
}
|
||||
|
||||
fun verificationStateIdleResource(transactionId: String, checkForState: VerificationTxState, session: Session): IdlingResource {
|
||||
val idle = object : IdlingResource, VerificationService.Listener {
|
||||
private var callback: IdlingResource.ResourceCallback? = null
|
||||
|
||||
private var currentState: VerificationTxState? = null
|
||||
|
||||
override fun getName() = "verificationSuccessIdle"
|
||||
|
||||
override fun isIdleNow(): Boolean {
|
||||
return currentState == checkForState
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||
this.callback = callback
|
||||
}
|
||||
|
||||
fun update(state: VerificationTxState) {
|
||||
currentState = state
|
||||
if (state == checkForState) {
|
||||
session.cryptoService().verificationService().removeListener(this)
|
||||
callback?.onTransitionToIdle()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a transaction is created, either by the user or initiated by the other user.
|
||||
*/
|
||||
override fun transactionCreated(tx: VerificationTransaction) {
|
||||
if (tx.transactionId == transactionId) update(tx.state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a transaction is updated. You may be interested to track the state of the VerificationTransaction.
|
||||
*/
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if (tx.transactionId == transactionId) update(tx.state)
|
||||
}
|
||||
}
|
||||
|
||||
session.cryptoService().verificationService().addListener(idle)
|
||||
return idle
|
||||
}
|
||||
|
||||
object UITestVerificationUtils
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
||||
import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask
|
||||
import im.vector.app.features.crypto.recover.Params
|
||||
import im.vector.app.features.home.HomeActivity
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.matrix.android.sdk.api.Matrix
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class VerifySessionPassphraseTest : VerificationTestBase() {
|
||||
|
||||
var existingSession: Session? = null
|
||||
val passphrase = "person woman camera tv"
|
||||
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||
|
||||
@Before
|
||||
fun createSessionWithCrossSigningAnd4S() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val matrix = Matrix.getInstance(context)
|
||||
val userName = "foobar_${System.currentTimeMillis()}"
|
||||
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||
doSync<Unit> {
|
||||
existingSession!!.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(UserPasswordAuth(
|
||||
user = existingSession!!.myUserId,
|
||||
password = "password"
|
||||
), it)
|
||||
}
|
||||
|
||||
val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources))
|
||||
|
||||
runBlocking {
|
||||
task.execute(Params(
|
||||
userPasswordAuth = UserPasswordAuth(password = password),
|
||||
passphrase = passphrase
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkVerifyWithPassphrase() {
|
||||
val userId: String = existingSession!!.myUserId
|
||||
|
||||
doLogin(homeServerUrl, userId, password)
|
||||
|
||||
// Thread.sleep(6000)
|
||||
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||
onView(withId(R.id.roomListContainer))
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(closeSoftKeyboard())
|
||||
}
|
||||
|
||||
val activity = EspressoHelper.getCurrentActivity()!!
|
||||
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
|
||||
|
||||
withIdlingResource(initialSyncIdlingResource(uiSession)) {
|
||||
onView(withId(R.id.roomListContainer))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
|
||||
// Cannot wait for view because of alerter animation? ...
|
||||
Thread.sleep(6000)
|
||||
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
|
||||
activity.runOnUiThread {
|
||||
popup.performClick()
|
||||
}
|
||||
|
||||
onView(withId(R.id.bottomSheetFragmentContainer))
|
||||
.check(matches(isDisplayed()))
|
||||
|
||||
onView(isRoot()).perform(SleepViewAction.sleep(2000))
|
||||
|
||||
onView(withText(R.string.use_latest_app))
|
||||
.check(matches(isDisplayed()))
|
||||
|
||||
// 4S is not setup so passphrase option should be hidden
|
||||
onView(withId(R.id.bottomSheetFragmentContainer))
|
||||
.check(matches(hasDescendant(withText(R.string.verification_cannot_access_other_session))))
|
||||
|
||||
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||
.perform(
|
||||
actionOnItem<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText(R.string.verification_cannot_access_other_session)),
|
||||
click()
|
||||
)
|
||||
)
|
||||
|
||||
withIdlingResource(activityIdlingResource(SharedSecureStorageActivity::class.java)) {
|
||||
onView(withId(R.id.ssss__root)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
onView((withId(R.id.ssss_passphrase_enter_edittext)))
|
||||
.perform(typeText(passphrase))
|
||||
|
||||
onView((withId(R.id.ssss_passphrase_submit)))
|
||||
.perform(click())
|
||||
|
||||
System.out.println("*** passphrase 1")
|
||||
|
||||
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||
System.out.println("*** passphrase 1.1")
|
||||
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||
.check(
|
||||
matches(hasDescendant(withText(R.string.verification_conclusion_ok_self_notice)))
|
||||
)
|
||||
}
|
||||
|
||||
System.out.println("*** passphrase 2")
|
||||
// check that all secrets are known?
|
||||
assert(uiSession.cryptoService().crossSigningService().canCrossSign())
|
||||
assert(uiSession.cryptoService().crossSigningService().allPrivateKeysKnown())
|
||||
|
||||
Thread.sleep(10_000)
|
||||
}
|
||||
}
|
@ -17,7 +17,10 @@
|
||||
package im.vector.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
@ -92,6 +95,15 @@ class VectorApplication :
|
||||
// font thread handler
|
||||
private var fontThreadHandler: Handler? = null
|
||||
|
||||
private val powerKeyReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_SCREEN_OFF
|
||||
&& vectorPreferences.useFlagPinCode()) {
|
||||
pinLocker.screenIsOff()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
enableStrictModeIfNeeded()
|
||||
super.onCreate()
|
||||
@ -163,6 +175,12 @@ class VectorApplication :
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
|
||||
// This should be done as early as possible
|
||||
// initKnownEmojiHashSet(appContext)
|
||||
|
||||
applicationContext.registerReceiver(powerKeyReceiver, IntentFilter().apply {
|
||||
// Looks like i cannot receive OFF, if i don't have both ON and OFF
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
})
|
||||
}
|
||||
|
||||
private fun enableStrictModeIfNeeded() {
|
||||
|
@ -89,6 +89,7 @@ import im.vector.app.features.settings.VectorSettingsHelpAboutFragment
|
||||
import im.vector.app.features.settings.VectorSettingsLabsFragment
|
||||
import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment
|
||||
import im.vector.app.features.settings.VectorSettingsNotificationsTroubleshootFragment
|
||||
import im.vector.app.features.settings.VectorSettingsPinFragment
|
||||
import im.vector.app.features.settings.VectorSettingsPreferencesFragment
|
||||
import im.vector.app.features.settings.VectorSettingsSecurityPrivacyFragment
|
||||
import im.vector.app.features.settings.account.deactivation.DeactivateAccountFragment
|
||||
@ -284,6 +285,11 @@ interface FragmentModule {
|
||||
@FragmentKey(VectorSettingsLabsFragment::class)
|
||||
fun bindVectorSettingsLabsFragment(fragment: VectorSettingsLabsFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(VectorSettingsPinFragment::class)
|
||||
fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(PushRulesFragment::class)
|
||||
|
@ -318,11 +318,17 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
||||
if (requestCode == PinActivity.PIN_REQUEST_CODE) {
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
Timber.v("Pin ok, unlock app")
|
||||
pinLocker.unlock()
|
||||
|
||||
// Cancel any new started PinActivity, after a screen rotation for instance
|
||||
finishActivity(PinActivity.PIN_REQUEST_CODE)
|
||||
}
|
||||
else -> {
|
||||
pinLocker.block()
|
||||
moveTaskToBack(true)
|
||||
if (pinLocker.getLiveState().value != PinLocker.State.UNLOCKED) {
|
||||
// Remove the task, to be sure that PIN code will be requested when resumed
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,10 @@ fun isAirplaneModeOn(context: Context): Boolean {
|
||||
return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0
|
||||
}
|
||||
|
||||
fun isAnimationDisabled(context: Context): Boolean {
|
||||
return Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* display the system dialog for granting this permission. If previously granted, the
|
||||
* system will not show it (so you should call this method).
|
||||
|
@ -31,6 +31,7 @@ import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import im.vector.app.features.discovery.change.SetIdentityServerFragment
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
import im.vector.app.features.terms.ReviewTermsActivity
|
||||
import org.matrix.android.sdk.api.session.identity.SharedState
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
@ -178,10 +179,6 @@ class DiscoverySettingsFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun navigateToChangeIdentityServerFragment() {
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom)
|
||||
.replace(R.id.vector_settings_page, SetIdentityServerFragment::class.java, null)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
(vectorBaseActivity as? VectorSettingsActivity)?.navigateTo(SetIdentityServerFragment::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import butterknife.BindView
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.OnModelBuildFinishedListener
|
||||
import com.airbnb.epoxy.addGlidePreloader
|
||||
import com.airbnb.epoxy.glidePreloader
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
@ -75,6 +77,7 @@ import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.extensions.showKeyboard
|
||||
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
@ -218,7 +221,8 @@ class RoomDetailFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider,
|
||||
private val notificationUtils: NotificationUtils,
|
||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
|
||||
private val matrixItemColorProvider: MatrixItemColorProvider
|
||||
private val matrixItemColorProvider: MatrixItemColorProvider,
|
||||
private val imageContentRenderer: ImageContentRenderer
|
||||
) :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
@ -921,6 +925,16 @@ class RoomDetailFragment @Inject constructor(
|
||||
val touchHelper = ItemTouchHelper(swipeCallback)
|
||||
touchHelper.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
recyclerView.addGlidePreloader(
|
||||
epoxyController = timelineEventController,
|
||||
requestManager = GlideApp.with(this),
|
||||
preloader = glidePreloader { requestManager, epoxyModel: MessageImageVideoItem, _ ->
|
||||
imageContentRenderer.createGlideRequest(
|
||||
epoxyModel.mediaData,
|
||||
ImageContentRenderer.Mode.THUMBNAIL,
|
||||
requestManager as GlideRequests
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateJumpToReadMarkerViewVisibility() {
|
||||
|
@ -56,6 +56,8 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val DEFAULT_PREFETCH_THRESHOLD = 30
|
||||
|
||||
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
||||
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
|
||||
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
|
||||
@ -116,6 +118,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private var unreadState: UnreadState = UnreadState.Unknown
|
||||
private var positionOfReadMarker: Int? = null
|
||||
private var eventIdToHighlight: String? = null
|
||||
private var previousModelsSize = 0
|
||||
|
||||
var callback: Callback? = null
|
||||
var timeline: Timeline? = null
|
||||
@ -191,6 +194,29 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
models.add(position, readMarker)
|
||||
}
|
||||
}
|
||||
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
|
||||
if (shouldAddBackwardPrefetch) {
|
||||
val indexOfPrefetchBackward = (previousModelsSize - 1)
|
||||
.coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_backward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
|
||||
|
||||
models.add(indexOfPrefetchBackward, loadingItem)
|
||||
}
|
||||
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
|
||||
if (shouldAddForwardPrefetch) {
|
||||
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_forward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
|
||||
models.add(indexOfPrefetchForward, loadingItem)
|
||||
}
|
||||
previousModelsSize = models.size
|
||||
}
|
||||
|
||||
fun update(viewState: RoomDetailViewState) {
|
||||
@ -355,9 +381,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
return shouldAdd
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if added
|
||||
*/
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
||||
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
@ -43,30 +44,33 @@ class MatrixItemColorProvider @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
private fun getColorFromUserId(userId: String?): Int {
|
||||
var hash = 0
|
||||
companion object {
|
||||
@ColorRes
|
||||
@VisibleForTesting
|
||||
fun getColorFromUserId(userId: String?): Int {
|
||||
var hash = 0
|
||||
|
||||
userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.toInt() }
|
||||
userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.toInt() }
|
||||
|
||||
return when (abs(hash) % 8) {
|
||||
1 -> R.color.riotx_username_2
|
||||
2 -> R.color.riotx_username_3
|
||||
3 -> R.color.riotx_username_4
|
||||
4 -> R.color.riotx_username_5
|
||||
5 -> R.color.riotx_username_6
|
||||
6 -> R.color.riotx_username_7
|
||||
7 -> R.color.riotx_username_8
|
||||
else -> R.color.riotx_username_1
|
||||
return when (abs(hash) % 8) {
|
||||
1 -> R.color.riotx_username_2
|
||||
2 -> R.color.riotx_username_3
|
||||
3 -> R.color.riotx_username_4
|
||||
4 -> R.color.riotx_username_5
|
||||
5 -> R.color.riotx_username_6
|
||||
6 -> R.color.riotx_username_7
|
||||
7 -> R.color.riotx_username_8
|
||||
else -> R.color.riotx_username_1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
private fun getColorFromRoomId(roomId: String?): Int {
|
||||
return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) {
|
||||
1 -> R.color.riotx_avatar_fill_2
|
||||
2 -> R.color.riotx_avatar_fill_3
|
||||
else -> R.color.riotx_avatar_fill_1
|
||||
@ColorRes
|
||||
private fun getColorFromRoomId(roomId: String?): Int {
|
||||
return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) {
|
||||
1 -> R.color.riotx_avatar_fill_2
|
||||
2 -> R.color.riotx_avatar_fill_3
|
||||
else -> R.color.riotx_avatar_fill_1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,8 +146,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
|
||||
LoginServerSelectionFragment::class.java,
|
||||
option = { ft ->
|
||||
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// Disable transition of text
|
||||
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// No transition here now actually
|
||||
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
|
||||
// TODO Disabled because it provokes a flickering
|
||||
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
|
||||
})
|
||||
|
@ -33,6 +33,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.glide.GlideRequest
|
||||
import im.vector.app.core.glide.GlideRequests
|
||||
import im.vector.app.core.ui.model.Size
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.core.utils.isLocalFile
|
||||
@ -206,12 +207,14 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||
fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||
return createGlideRequest(data, mode, GlideApp.with(imageView), size)
|
||||
}
|
||||
|
||||
fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> {
|
||||
return if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(data)
|
||||
glideRequests.load(data)
|
||||
} else {
|
||||
// Clear image
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
@ -223,15 +226,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
// Fallback to base url
|
||||
?: data.url.takeIf { it?.startsWith("content://") == true }
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
glideRequests
|
||||
.load(resolvedUrl)
|
||||
.apply {
|
||||
if (mode == Mode.THUMBNAIL) {
|
||||
error(
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(resolveUrl(data))
|
||||
glideRequests.load(resolveUrl(data))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,9 @@ import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import me.gujun.android.span.span
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@ -72,6 +72,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
private val currentSession: Session?
|
||||
get() = activeSessionDataSource.currentValue?.orNull()
|
||||
|
||||
private var useCompleteNotificationFormat = vectorPreferences.useCompleteNotificationFormat()
|
||||
|
||||
/**
|
||||
Should be called as soon as a new event is ready to be displayed.
|
||||
The notification corresponding to this event will not be displayed until
|
||||
@ -243,8 +245,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
roomEvents.add(event)
|
||||
}
|
||||
}
|
||||
is InviteNotifiableEvent -> invitationEvents.add(event)
|
||||
is SimpleNotifiableEvent -> simpleEvents.add(event)
|
||||
is InviteNotifiableEvent -> invitationEvents.add(event)
|
||||
is SimpleNotifiableEvent -> simpleEvents.add(event)
|
||||
else -> Timber.w("Type not handled")
|
||||
}
|
||||
}
|
||||
@ -253,6 +255,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
|
||||
var globalLastMessageTimestamp = 0L
|
||||
|
||||
val newSettings = vectorPreferences.useCompleteNotificationFormat()
|
||||
if (newSettings != useCompleteNotificationFormat) {
|
||||
// Settings has changed, remove all current notifications
|
||||
notificationUtils.cancelAllNotifications()
|
||||
useCompleteNotificationFormat = newSettings
|
||||
}
|
||||
|
||||
var simpleNotificationRoomCounter = 0
|
||||
var simpleNotificationMessageCounter = 0
|
||||
|
||||
// events have been grouped by roomId
|
||||
for ((roomId, events) in roomIdToEventMap) {
|
||||
// Build the notification for the room
|
||||
@ -263,6 +275,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
continue
|
||||
}
|
||||
|
||||
simpleNotificationRoomCounter++
|
||||
val roomName = events[0].roomName ?: events[0].senderName ?: ""
|
||||
|
||||
val roomEventGroupInfo = RoomEventGroupInfo(
|
||||
@ -303,6 +316,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
roomEventGroupInfo.hasSmartReplyError = true
|
||||
} else {
|
||||
if (!event.isRedacted) {
|
||||
simpleNotificationMessageCounter++
|
||||
style.addMessage(event.body, event.timestamp, senderPerson)
|
||||
}
|
||||
}
|
||||
@ -361,16 +375,18 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description)
|
||||
}
|
||||
|
||||
val notification = notificationUtils.buildMessagesListNotification(
|
||||
style,
|
||||
roomEventGroupInfo,
|
||||
largeBitmap,
|
||||
lastMessageTimestamp,
|
||||
myUserDisplayName,
|
||||
tickerText)
|
||||
if (useCompleteNotificationFormat) {
|
||||
val notification = notificationUtils.buildMessagesListNotification(
|
||||
style,
|
||||
roomEventGroupInfo,
|
||||
largeBitmap,
|
||||
lastMessageTimestamp,
|
||||
myUserDisplayName,
|
||||
tickerText)
|
||||
|
||||
// is there an id for this room?
|
||||
notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification)
|
||||
// is there an id for this room?
|
||||
notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
hasNewEvent = true
|
||||
summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing
|
||||
@ -383,8 +399,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
for (event in invitationEvents) {
|
||||
// We build a invitation notification
|
||||
if (firstTime || !event.hasBeenDisplayed) {
|
||||
val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId)
|
||||
notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification)
|
||||
if (useCompleteNotificationFormat) {
|
||||
val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId)
|
||||
notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification)
|
||||
}
|
||||
event.hasBeenDisplayed = true // we can consider it as displayed
|
||||
hasNewEvent = true
|
||||
summaryIsNoisy = summaryIsNoisy || event.noisy
|
||||
@ -396,8 +414,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
for (event in simpleEvents) {
|
||||
// We build a simple notification
|
||||
if (firstTime || !event.hasBeenDisplayed) {
|
||||
val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId)
|
||||
notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification)
|
||||
if (useCompleteNotificationFormat) {
|
||||
val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId)
|
||||
notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification)
|
||||
}
|
||||
event.hasBeenDisplayed = true // we can consider it as displayed
|
||||
hasNewEvent = true
|
||||
summaryIsNoisy = summaryIsNoisy || event.noisy
|
||||
@ -421,19 +441,76 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||
if (eventList.isEmpty() || eventList.all { it.isRedacted }) {
|
||||
notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID)
|
||||
} else {
|
||||
// FIXME roomIdToEventMap.size is not correct, this is the number of rooms
|
||||
val nbEvents = roomIdToEventMap.size + simpleEvents.size
|
||||
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
|
||||
summaryInboxStyle.setBigContentTitle(sumTitle)
|
||||
// TODO get latest event?
|
||||
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
|
||||
|
||||
val notification = notificationUtils.buildSummaryListNotification(
|
||||
summaryInboxStyle,
|
||||
sumTitle,
|
||||
noisy = hasNewEvent && summaryIsNoisy,
|
||||
lastMessageTimestamp = globalLastMessageTimestamp)
|
||||
if (useCompleteNotificationFormat) {
|
||||
val notification = notificationUtils.buildSummaryListNotification(
|
||||
summaryInboxStyle,
|
||||
sumTitle,
|
||||
noisy = hasNewEvent && summaryIsNoisy,
|
||||
lastMessageTimestamp = globalLastMessageTimestamp)
|
||||
|
||||
notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification)
|
||||
notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification)
|
||||
} else {
|
||||
// Add the simple events as message (?)
|
||||
simpleNotificationMessageCounter += simpleEvents.size
|
||||
val numberOfInvitations = invitationEvents.size
|
||||
|
||||
val privacyTitle = if (numberOfInvitations > 0) {
|
||||
val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, numberOfInvitations, numberOfInvitations)
|
||||
if (simpleNotificationMessageCounter > 0) {
|
||||
// Invitation and message
|
||||
val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification,
|
||||
simpleNotificationMessageCounter, simpleNotificationMessageCounter)
|
||||
if (simpleNotificationRoomCounter > 1) {
|
||||
// In several rooms
|
||||
val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms,
|
||||
simpleNotificationRoomCounter, simpleNotificationRoomCounter)
|
||||
stringProvider.getString(
|
||||
R.string.notification_unread_notified_messages_in_room_and_invitation,
|
||||
messageStr,
|
||||
roomStr,
|
||||
invitationsStr
|
||||
)
|
||||
} else {
|
||||
// In one room
|
||||
stringProvider.getString(
|
||||
R.string.notification_unread_notified_messages_and_invitation,
|
||||
messageStr,
|
||||
invitationsStr
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Only invitation
|
||||
invitationsStr
|
||||
}
|
||||
} else {
|
||||
// No invitation, only messages
|
||||
val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification,
|
||||
simpleNotificationMessageCounter, simpleNotificationMessageCounter)
|
||||
if (simpleNotificationRoomCounter > 1) {
|
||||
// In several rooms
|
||||
val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms,
|
||||
simpleNotificationRoomCounter, simpleNotificationRoomCounter)
|
||||
stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr)
|
||||
} else {
|
||||
// In one room
|
||||
messageStr
|
||||
}
|
||||
}
|
||||
val notification = notificationUtils.buildSummaryListNotification(
|
||||
style = null,
|
||||
compatSummary = privacyTitle,
|
||||
noisy = hasNewEvent && summaryIsNoisy,
|
||||
lastMessageTimestamp = globalLastMessageTimestamp)
|
||||
|
||||
notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
if (hasNewEvent && summaryIsNoisy) {
|
||||
try {
|
||||
|
@ -772,7 +772,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
/**
|
||||
* Build the summary notification
|
||||
*/
|
||||
fun buildSummaryListNotification(style: NotificationCompat.InboxStyle,
|
||||
fun buildSummaryListNotification(style: NotificationCompat.InboxStyle?,
|
||||
compatSummary: String,
|
||||
noisy: Boolean,
|
||||
lastMessageTimestamp: Long): Notification {
|
||||
|
@ -28,7 +28,6 @@ import im.vector.app.core.platform.VectorBaseActivity
|
||||
class PinActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity {
|
||||
|
||||
companion object {
|
||||
|
||||
const val PIN_REQUEST_CODE = 17890
|
||||
|
||||
fun newIntent(context: Context, args: PinArgs): Intent {
|
||||
|
@ -32,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.MainActivityArgs
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@ -42,7 +43,8 @@ data class PinArgs(
|
||||
) : Parcelable
|
||||
|
||||
class PinFragment @Inject constructor(
|
||||
private val pinCodeStore: PinCodeStore
|
||||
private val pinCodeStore: PinCodeStore,
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
private val fragmentArgs: PinArgs by args()
|
||||
@ -53,54 +55,10 @@ class PinFragment @Inject constructor(
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
when (fragmentArgs.pinMode) {
|
||||
PinMode.CREATE -> showCreateFragment()
|
||||
PinMode.DELETE -> showDeleteFragment()
|
||||
PinMode.AUTH -> showAuthFragment()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDeleteFragment() {
|
||||
val encodedPin = pinCodeStore.getEncodedPin() ?: return
|
||||
val authFragment = PFLockScreenFragment()
|
||||
val builder = PFFLockScreenConfiguration.Builder(requireContext())
|
||||
.setUseBiometric(pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0)
|
||||
.setTitle(getString(R.string.auth_pin_confirm_to_disable_title))
|
||||
.setClearCodeOnError(true)
|
||||
.setMode(PFFLockScreenConfiguration.MODE_AUTH)
|
||||
authFragment.setConfiguration(builder.build())
|
||||
authFragment.setEncodedPinCode(encodedPin)
|
||||
authFragment.setLoginListener(object : PFLockScreenFragment.OnPFLockScreenLoginListener {
|
||||
override fun onPinLoginFailed() {
|
||||
onWrongPin()
|
||||
}
|
||||
|
||||
override fun onBiometricAuthSuccessful() {
|
||||
lifecycleScope.launch {
|
||||
pinCodeStore.deleteEncodedPin()
|
||||
vectorBaseActivity.setResult(Activity.RESULT_OK)
|
||||
vectorBaseActivity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBiometricAuthLoginFailed() {
|
||||
val remainingAttempts = pinCodeStore.onWrongBiometrics()
|
||||
if (remainingAttempts <= 0) {
|
||||
// Disable Biometrics
|
||||
builder.setUseBiometric(false)
|
||||
authFragment.setConfiguration(builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCodeInputSuccessful() {
|
||||
lifecycleScope.launch {
|
||||
pinCodeStore.deleteEncodedPin()
|
||||
vectorBaseActivity.setResult(Activity.RESULT_OK)
|
||||
vectorBaseActivity.finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
replaceFragment(R.id.pinFragmentContainer, authFragment)
|
||||
}
|
||||
|
||||
private fun showCreateFragment() {
|
||||
val createFragment = PFLockScreenFragment()
|
||||
val builder = PFFLockScreenConfiguration.Builder(requireContext())
|
||||
@ -131,9 +89,8 @@ class PinFragment @Inject constructor(
|
||||
val authFragment = PFLockScreenFragment()
|
||||
val canUseBiometrics = pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0
|
||||
val builder = PFFLockScreenConfiguration.Builder(requireContext())
|
||||
.setUseBiometric(true)
|
||||
.setAutoShowBiometric(true)
|
||||
.setUseBiometric(canUseBiometrics)
|
||||
.setUseBiometric(vectorPreferences.useBiometricsToUnlock() && canUseBiometrics)
|
||||
.setAutoShowBiometric(canUseBiometrics)
|
||||
.setTitle(getString(R.string.auth_pin_title))
|
||||
.setLeftButton(getString(R.string.auth_pin_forgot))
|
||||
|
@ -22,12 +22,14 @@ import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
// 2 minutes, when enabled
|
||||
private const val PERIOD_OF_GRACE_IN_MS = 2 * 60 * 1000L
|
||||
|
||||
/**
|
||||
@ -35,24 +37,22 @@ private const val PERIOD_OF_GRACE_IN_MS = 2 * 60 * 1000L
|
||||
* It automatically locks when entering background/foreground with a grace period.
|
||||
* You can force to unlock with unlock method, use it whenever the pin code has been validated.
|
||||
*/
|
||||
|
||||
@Singleton
|
||||
class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : LifecycleObserver {
|
||||
class PinLocker @Inject constructor(
|
||||
private val pinCodeStore: PinCodeStore,
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : LifecycleObserver {
|
||||
|
||||
enum class State {
|
||||
// App is locked, can be unlock
|
||||
LOCKED,
|
||||
|
||||
// App is blocked and can't be unlocked as long as the app is in foreground
|
||||
BLOCKED,
|
||||
|
||||
// is unlocked, the app can be used
|
||||
// App is unlocked, the app can be used
|
||||
UNLOCKED
|
||||
}
|
||||
|
||||
private val liveState = MutableLiveData<State>()
|
||||
|
||||
private var isBlocked = false
|
||||
private var shouldBeLocked = true
|
||||
private var entersBackgroundTs = 0L
|
||||
|
||||
@ -62,13 +62,13 @@ class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : Li
|
||||
|
||||
private fun computeState() {
|
||||
GlobalScope.launch {
|
||||
val state = if (isBlocked) {
|
||||
State.BLOCKED
|
||||
} else if (shouldBeLocked && pinCodeStore.hasEncodedPin()) {
|
||||
val state = if (shouldBeLocked && pinCodeStore.hasEncodedPin()) {
|
||||
State.LOCKED
|
||||
} else {
|
||||
State.UNLOCKED
|
||||
}
|
||||
.also { Timber.v("New state: $it") }
|
||||
|
||||
if (liveState.value != state) {
|
||||
liveState.postValue(state)
|
||||
}
|
||||
@ -81,23 +81,30 @@ class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : Li
|
||||
computeState()
|
||||
}
|
||||
|
||||
fun block() {
|
||||
Timber.v("Block app")
|
||||
isBlocked = true
|
||||
fun screenIsOff() {
|
||||
shouldBeLocked = true
|
||||
computeState()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs
|
||||
shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= PERIOD_OF_GRACE_IN_MS
|
||||
Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background")
|
||||
shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= getGracePeriod()
|
||||
Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background shouldBeLocked: $shouldBeLocked")
|
||||
computeState()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
isBlocked = false
|
||||
Timber.v("App enters background")
|
||||
entersBackgroundTs = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
||||
private fun getGracePeriod(): Long {
|
||||
return if (vectorPreferences.useGracePeriod()) {
|
||||
PERIOD_OF_GRACE_IN_MS
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,5 @@ package im.vector.app.features.pin
|
||||
|
||||
enum class PinMode {
|
||||
CREATE,
|
||||
DELETE,
|
||||
AUTH
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import com.tapadoo.alerter.OnHideAlertListener
|
||||
import dagger.Lazy
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.utils.isAnimationDisabled
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
@ -172,6 +173,8 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {
|
||||
clearLightStatusBar()
|
||||
|
||||
val noAnimation = !animate || isAnimationDisabled(activity)
|
||||
|
||||
alert.weakCurrentActivity = WeakReference(activity)
|
||||
val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout)
|
||||
else Alerter.create(activity)
|
||||
@ -187,7 +190,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||
}
|
||||
}
|
||||
.apply {
|
||||
if (!animate) {
|
||||
if (noAnimation) {
|
||||
setEnterAnimation(R.anim.anim_alerter_no_anim)
|
||||
}
|
||||
|
||||
@ -237,6 +240,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||
setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color)
|
||||
}
|
||||
}
|
||||
.enableIconPulse(!noAnimation)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.app.features.raw.wellknown
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
object ElementWellKnownMapper {
|
||||
@ -24,6 +25,6 @@ object ElementWellKnownMapper {
|
||||
val adapter: JsonAdapter<ElementWellKnown> = MoshiProvider.providesMoshi().adapter(ElementWellKnown::class.java)
|
||||
|
||||
fun from(value: String): ElementWellKnown? {
|
||||
return adapter.fromJson(value)
|
||||
return tryOrNull("Unable to parse well-known data") { adapter.fromJson(value) }
|
||||
}
|
||||
}
|
||||
|
@ -166,6 +166,9 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
// Security
|
||||
const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE"
|
||||
const val SETTINGS_SECURITY_USE_PIN_CODE_FLAG = "SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
|
||||
private const val SETTINGS_SECURITY_USE_BIOMETRICS_FLAG = "SETTINGS_SECURITY_USE_BIOMETRICS_FLAG"
|
||||
private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG"
|
||||
const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG"
|
||||
|
||||
// other
|
||||
const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
|
||||
@ -839,12 +842,29 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The user enable protecting app access with pin code
|
||||
* The user enable protecting app access with pin code.
|
||||
* Currently we use the pin code store to know if the pin is enabled, so this is not used
|
||||
*/
|
||||
fun useFlagPinCode(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false)
|
||||
}
|
||||
|
||||
fun useBiometricsToUnlock(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_BIOMETRICS_FLAG, true)
|
||||
}
|
||||
|
||||
fun useGracePeriod(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if Pin code is disabled, or if user set the settings to see full notification content
|
||||
*/
|
||||
fun useCompleteNotificationFormat(): Boolean {
|
||||
return !useFlagPinCode()
|
||||
|| defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG, true)
|
||||
}
|
||||
|
||||
fun backgroundSyncTimeOut(): Int {
|
||||
return tryOrNull {
|
||||
// The xml pref is saved as a string so use getString and parse
|
||||
|
@ -17,6 +17,7 @@ package im.vector.app.features.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
@ -134,6 +135,14 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Fragment> navigateTo(fragmentClass: Class<T>) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.right_in, R.anim.fade_out, R.anim.fade_in, R.anim.right_out)
|
||||
.replace(R.id.vector_settings_page, fragmentClass, null)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
|
||||
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
|
||||
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2019 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.Intent
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.SwitchPreference
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
import im.vector.app.features.pin.PinCodeStore
|
||||
import im.vector.app.features.pin.PinMode
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class VectorSettingsPinFragment @Inject constructor(
|
||||
private val pinCodeStore: PinCodeStore,
|
||||
private val navigator: Navigator,
|
||||
private val notificationDrawerManager: NotificationDrawerManager
|
||||
) : VectorSettingsBaseFragment() {
|
||||
|
||||
override var titleRes = R.string.settings_security_application_protection_screen_title
|
||||
override val preferenceXmlRes = R.xml.vector_settings_pin
|
||||
|
||||
private val usePinCodePref by lazy {
|
||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!!
|
||||
}
|
||||
|
||||
private val useCompleteNotificationPref by lazy {
|
||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG)!!
|
||||
}
|
||||
|
||||
override fun bindPref() {
|
||||
refreshPinCodeStatus()
|
||||
|
||||
useCompleteNotificationPref.setOnPreferenceChangeListener { _, _ ->
|
||||
// Refresh the drawer for an immediate effect of this change
|
||||
notificationDrawerManager.refreshNotificationDrawer()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshPinCodeStatus() {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
val hasPinCode = pinCodeStore.hasEncodedPin()
|
||||
usePinCodePref.isChecked = hasPinCode
|
||||
usePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
if (hasPinCode) {
|
||||
lifecycleScope.launch {
|
||||
pinCodeStore.deleteEncodedPin()
|
||||
refreshPinCodeStatus()
|
||||
}
|
||||
} else {
|
||||
navigator.openPinCode(this@VectorSettingsPinFragment, PinMode.CREATE)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == PinActivity.PIN_REQUEST_CODE) {
|
||||
refreshPinCodeStatus()
|
||||
}
|
||||
}
|
||||
}
|
@ -56,9 +56,8 @@ import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import im.vector.app.features.pin.PinActivity
|
||||
import im.vector.app.features.pin.PinCodeStore
|
||||
import im.vector.app.features.pin.PinLocker
|
||||
import im.vector.app.features.pin.PinMode
|
||||
import im.vector.app.features.raw.wellknown.ElementWellKnownMapper
|
||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||
import im.vector.app.features.raw.wellknown.isE2EByDefault
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
@ -76,7 +75,6 @@ import javax.inject.Inject
|
||||
|
||||
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val pinLocker: PinLocker,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val pinCodeStore: PinCodeStore,
|
||||
private val navigator: Navigator
|
||||
@ -128,8 +126,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
|
||||
}
|
||||
|
||||
private val usePinCodePref by lazy {
|
||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!!
|
||||
private val openPinCodeSettingsPref by lazy {
|
||||
findPreference<VectorPreference>("SETTINGS_SECURITY_PIN")!!
|
||||
}
|
||||
|
||||
override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView {
|
||||
@ -155,14 +153,13 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
disposables.add(it)
|
||||
}
|
||||
|
||||
vectorActivity.getVectorComponent()
|
||||
.rawService()
|
||||
.getWellknown(session.myUserId, object : MatrixCallback<String> {
|
||||
override fun onSuccess(data: String) {
|
||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible =
|
||||
ElementWellKnownMapper.from(data)?.isE2EByDefault() == false
|
||||
}
|
||||
})
|
||||
lifecycleScope.launchWhenResumed {
|
||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible =
|
||||
vectorActivity.getVectorComponent()
|
||||
.rawService()
|
||||
.getElementWellknown(session.myUserId)
|
||||
?.isE2EByDefault() == false
|
||||
}
|
||||
}
|
||||
|
||||
private val secureBackupCategory by lazy {
|
||||
@ -265,7 +262,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
refreshPinCodeStatus()
|
||||
openPinCodeSettingsPref.setOnPreferenceClickListener {
|
||||
openPinCodePreferenceScreen()
|
||||
true
|
||||
}
|
||||
|
||||
refreshXSigningStatus()
|
||||
|
||||
@ -321,62 +321,64 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
|
||||
val uri = data?.data
|
||||
if (resultCode == Activity.RESULT_OK && uri != null) {
|
||||
activity?.let { activity ->
|
||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
displayLoadingView()
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
when (requestCode) {
|
||||
REQUEST_CODE_SAVE_MEGOLM_EXPORT -> {
|
||||
val uri = data?.data
|
||||
if (uri != null) {
|
||||
activity?.let { activity ->
|
||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
displayLoadingView()
|
||||
|
||||
KeysExporter(session)
|
||||
.export(requireContext(),
|
||||
passphrase,
|
||||
uri,
|
||||
object : MatrixCallback<Boolean> {
|
||||
override fun onSuccess(data: Boolean) {
|
||||
if (data) {
|
||||
requireActivity().toast(getString(R.string.encryption_exported_successfully))
|
||||
} else {
|
||||
requireActivity().toast(getString(R.string.unexpected_error))
|
||||
}
|
||||
hideLoadingView()
|
||||
}
|
||||
KeysExporter(session)
|
||||
.export(requireContext(),
|
||||
passphrase,
|
||||
uri,
|
||||
object : MatrixCallback<Boolean> {
|
||||
override fun onSuccess(data: Boolean) {
|
||||
if (data) {
|
||||
requireActivity().toast(getString(R.string.encryption_exported_successfully))
|
||||
} else {
|
||||
requireActivity().toast(getString(R.string.unexpected_error))
|
||||
}
|
||||
hideLoadingView()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
onCommonDone(failure.localizedMessage)
|
||||
}
|
||||
})
|
||||
override fun onFailure(failure: Throwable) {
|
||||
onCommonDone(failure.localizedMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
PinActivity.PIN_REQUEST_CODE -> {
|
||||
doOpenPinCodePreferenceScreen()
|
||||
}
|
||||
REQUEST_E2E_FILE_REQUEST_CODE -> {
|
||||
importKeys(data)
|
||||
}
|
||||
}
|
||||
} else if (requestCode == PinActivity.PIN_REQUEST_CODE) {
|
||||
pinLocker.unlock()
|
||||
refreshPinCodeStatus()
|
||||
} else if (requestCode == REQUEST_E2E_FILE_REQUEST_CODE) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
importKeys(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshPinCodeStatus() {
|
||||
private fun openPinCodePreferenceScreen() {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
val hasPinCode = pinCodeStore.hasEncodedPin()
|
||||
usePinCodePref.isChecked = hasPinCode
|
||||
usePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val pinMode = if (hasPinCode) {
|
||||
PinMode.DELETE
|
||||
} else {
|
||||
PinMode.CREATE
|
||||
}
|
||||
navigator.openPinCode(this@VectorSettingsSecurityPrivacyFragment, pinMode)
|
||||
true
|
||||
if (hasPinCode) {
|
||||
navigator.openPinCode(this@VectorSettingsSecurityPrivacyFragment, PinMode.AUTH)
|
||||
} else {
|
||||
doOpenPinCodePreferenceScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doOpenPinCodePreferenceScreen() {
|
||||
(vectorActivity as? VectorSettingsActivity)?.navigateTo(VectorSettingsPinFragment::class.java)
|
||||
}
|
||||
|
||||
private fun refreshKeysManagementSection() {
|
||||
// If crypto is not enabled parent section will be removed
|
||||
// TODO notice that this will not work when no network
|
||||
|
@ -3,144 +3,187 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?riotx_background">
|
||||
android:background="?riotx_background"
|
||||
android:paddingStart="36dp"
|
||||
android:paddingTop="@dimen/layout_vertical_margin"
|
||||
android:paddingEnd="36dp"
|
||||
android:paddingBottom="@dimen/layout_vertical_margin">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
<!-- Strategy: 5 Spaces are used to spread the remaining space, using weight -->
|
||||
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashLogoContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="spread_inside"
|
||||
app:layout_constraintVertical_weight="4" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loginSplashLogoContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace1">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
style="@style/LoginContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashLogo"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:src="@drawable/element_logo_green"
|
||||
android:transitionName="loginLogoTransition" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashLogo"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:src="@drawable/element_logo_green"
|
||||
android:transitionName="loginLogoTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/logoType"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
<ImageView
|
||||
android:id="@+id/logoType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:src="@drawable/element_logotype"
|
||||
android:tint="?colorAccent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/logoType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:src="@drawable/element_logotype"
|
||||
android:tint="?colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/loginSplashLogo" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginSplashTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/login_splash_title"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
|
||||
android:transitionName="loginTitleTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashText1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/logoType" />
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashLogoContainer"
|
||||
app:layout_constraintVertical_weight="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto1"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_message_circle"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText1" />
|
||||
<TextView
|
||||
android:id="@+id/loginSplashTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/login_splash_title"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
|
||||
android:transitionName="loginTitleTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="96dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text1"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle" />
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashContent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashTitle"
|
||||
app:layout_constraintVertical_weight="2" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto2"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_lock"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="@id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText2" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/loginSplashContent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace4"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace3">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text2"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@id/loginSplashText3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/loginSplashText1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" />
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto1"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_message_circle"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_sliders"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="@+id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText3" />
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text1"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text3"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/loginSplashText1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" />
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto2"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_lock"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="@id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText2" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/loginSplashSubmit"
|
||||
style="@style/Style.Vector.Login.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:text="@string/login_splash_submit"
|
||||
android:transitionName="loginSubmitTransition"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText3" />
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text2"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toTopOf="@id/loginSplashText3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/loginSplashText1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText1" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<ImageView
|
||||
android:id="@+id/loginSplashPicto3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_login_splash_sliders"
|
||||
android:tint="?riotx_text_secondary"
|
||||
app:layout_constraintStart_toStartOf="@+id/loginSplashPicto1"
|
||||
app:layout_constraintTop_toTopOf="@+id/loginSplashText3" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
<TextView
|
||||
android:id="@+id/loginSplashText3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_splash_text3"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/loginSplashText1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashText2" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSubmit"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashContent"
|
||||
app:layout_constraintVertical_weight="2" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/loginSplashSubmit"
|
||||
style="@style/Style.Vector.Login.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_splash_submit"
|
||||
android:transitionName="loginSubmitTransition"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loginSplashSpace5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSpace4" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/loginSplashSpace5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/loginSplashSubmit"
|
||||
app:layout_constraintVertical_weight="4" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -178,7 +178,7 @@
|
||||
<string name="no_room_placeholder">No rooms</string>
|
||||
<string name="no_public_room_placeholder">No public rooms available</string>
|
||||
<plurals name="public_room_nb_users">
|
||||
<item quantity="one">1 user</item>
|
||||
<item quantity="one">%d user</item>
|
||||
<item quantity="other">%d users</item>
|
||||
</plurals>
|
||||
|
||||
@ -347,7 +347,7 @@
|
||||
|
||||
<!-- Mels -->
|
||||
<plurals name="membership_changes">
|
||||
<item quantity="one">1 membership change</item>
|
||||
<item quantity="one">%d membership change</item>
|
||||
<item quantity="other">%d membership changes</item>
|
||||
</plurals>
|
||||
|
||||
@ -450,30 +450,30 @@
|
||||
<string name="room_creation_title">New Chat</string>
|
||||
<string name="room_creation_add_member">Add member</string>
|
||||
<plurals name="room_header_active_members_count">
|
||||
<item quantity="one">1 active members</item>
|
||||
<item quantity="one">%d active members</item>
|
||||
<item quantity="other">%d active members</item>
|
||||
</plurals>
|
||||
<plurals name="room_title_members">
|
||||
<item quantity="one">1 member</item>
|
||||
<item quantity="one">%d member</item>
|
||||
<item quantity="other">%d members</item>
|
||||
</plurals>
|
||||
<string name="room_title_one_member">1 member</string>
|
||||
|
||||
<!-- Time -->
|
||||
<plurals name="format_time_s">
|
||||
<item quantity="one">1s</item>
|
||||
<item quantity="one">%ds</item>
|
||||
<item quantity="other">%ds</item>
|
||||
</plurals>
|
||||
<plurals name="format_time_m">
|
||||
<item quantity="one">1m</item>
|
||||
<item quantity="one">%dm</item>
|
||||
<item quantity="other">%dm</item>
|
||||
</plurals>
|
||||
<plurals name="format_time_h">
|
||||
<item quantity="one">1h</item>
|
||||
<item quantity="one">%dh</item>
|
||||
<item quantity="other">%dh</item>
|
||||
</plurals>
|
||||
<plurals name="format_time_d">
|
||||
<item quantity="one">1d</item>
|
||||
<item quantity="one">%dd</item>
|
||||
<item quantity="other">%dd</item>
|
||||
</plurals>
|
||||
|
||||
@ -581,7 +581,7 @@
|
||||
<string name="room_message_file_not_found">File not found</string>
|
||||
<string name="room_do_not_have_permission_to_post">You do not have permission to post to this room</string>
|
||||
<plurals name="room_new_messages_notification">
|
||||
<item quantity="one">1 new message</item>
|
||||
<item quantity="one">%d new message</item>
|
||||
<item quantity="other">%d new messages</item>
|
||||
</plurals>
|
||||
|
||||
@ -604,7 +604,7 @@
|
||||
<string name="room_details_files">Files</string>
|
||||
<string name="room_details_settings">Settings</string>
|
||||
<plurals name="room_details_selected">
|
||||
<item quantity="one">1 selected</item>
|
||||
<item quantity="one">%d selected</item>
|
||||
<item quantity="other">%d selected</item>
|
||||
</plurals>
|
||||
<string name="malformed_id">Malformed ID. Should be an email address or a Matrix ID like \'@localpart:domain\'</string>
|
||||
@ -642,7 +642,7 @@
|
||||
<!-- Directory -->
|
||||
<string name="directory_search_results_title">Browse directory</string>
|
||||
<plurals name="directory_search_rooms">
|
||||
<item quantity="one">1 room</item>
|
||||
<item quantity="one">%d room</item>
|
||||
<item quantity="other">%d rooms</item>
|
||||
</plurals>
|
||||
<plurals name="directory_search_rooms_for">
|
||||
@ -1143,20 +1143,24 @@
|
||||
|
||||
<!-- Notifications -->
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">1 unread notified message</item>
|
||||
<item quantity="one">%d unread notified message</item>
|
||||
<item quantity="other">%d unread notified messages</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_msgs">
|
||||
<item quantity="one">1 unread notified message</item>
|
||||
<item quantity="one">%d unread notified message</item>
|
||||
<item quantity="other">%d unread notified messages</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">1 room</item>
|
||||
<item quantity="one">%d room</item>
|
||||
<item quantity="other">%d rooms</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">%d invitation</item>
|
||||
<item quantity="other">%d invitations</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">%1$s: 1 message</item>
|
||||
<item quantity="one">%1$s: %2$d message</item>
|
||||
<item quantity="other">%1$s: %2$d messages</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
@ -1165,6 +1169,8 @@
|
||||
</plurals>
|
||||
|
||||
<string name="notification_unread_notified_messages_in_room">%1$s in %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">%1$s in %2$s and %3$s"</string>
|
||||
<string name="notification_unread_notified_messages_and_invitation">%1$s and %2$s"</string>
|
||||
<string name="notification_unknown_new_event">New Event</string>
|
||||
<string name="notification_unknown_room_name">Room</string>
|
||||
<string name="notification_new_messages">New Messages</string>
|
||||
@ -1193,7 +1199,7 @@
|
||||
<string name="settings_labs_create_conference_with_jitsi">Create conference calls with jitsi</string>
|
||||
<string name="widget_delete_message_confirmation">Are you sure you want to delete the widget from this room?</string>
|
||||
<plurals name="active_widgets">
|
||||
<item quantity="one">1 active widget</item>
|
||||
<item quantity="one">%d active widget</item>
|
||||
<item quantity="other">%d active widgets</item>
|
||||
</plurals>
|
||||
<string name="active_widget_view_action">"VIEW"</string>
|
||||
@ -1319,12 +1325,12 @@
|
||||
<string name="filter_group_rooms">Filter group rooms</string>
|
||||
|
||||
<plurals name="group_members">
|
||||
<item quantity="one">1 member</item>
|
||||
<item quantity="one">%d member</item>
|
||||
<item quantity="other">%d members</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="group_rooms">
|
||||
<item quantity="one">1 room</item>
|
||||
<item quantity="one">%d room</item>
|
||||
<item quantity="other">%d rooms</item>
|
||||
</plurals>
|
||||
<string name="group_no_long_description">The community admin has not provided a long description for this community.</string>
|
||||
@ -1809,7 +1815,7 @@
|
||||
<string name="two_users_read">%1$s and %2$s read</string>
|
||||
<string name="one_user_read">%s read</string>
|
||||
<plurals name="fallback_users_read">
|
||||
<item quantity="one">1 user read</item>
|
||||
<item quantity="one">%d user read</item>
|
||||
<item quantity="other">%d users read</item>
|
||||
</plurals>
|
||||
|
||||
@ -2560,14 +2566,26 @@
|
||||
<string name="too_many_pin_failures">Too many errors, you\'ve been logged out</string>
|
||||
<string name="create_pin_title">Choose a PIN for security</string>
|
||||
<string name="create_pin_confirm_title">Confirm PIN</string>
|
||||
<string name="create_pin_confirm_failure">Failed to validate pin, please tap a new one.</string>
|
||||
<string name="create_pin_confirm_failure">Failed to validate PIN, please tap a new one.</string>
|
||||
<string name="auth_pin_title">Enter your PIN</string>
|
||||
<string name="auth_pin_forgot">Forgot PIN?</string>
|
||||
<string name="auth_pin_reset_title">Reset pin</string>
|
||||
<string name="auth_pin_new_pin_action">New pin</string>
|
||||
<string name="auth_pin_reset_title">Reset PIN</string>
|
||||
<string name="auth_pin_new_pin_action">New PIN</string>
|
||||
<string name="auth_pin_reset_content">To reset your PIN, you\'ll need to re-login and create a new one.</string>
|
||||
<string name="settings_security_application_protection_title">Protect access</string>
|
||||
<string name="settings_security_application_protection_summary">Protect access using PIN and biometrics.</string>
|
||||
<string name="settings_security_application_protection_screen_title">Configure protection</string>
|
||||
<string name="settings_security_pin_code_title">Enable PIN</string>
|
||||
<string name="settings_security_pin_code_summary">If you want to reset your PIN, tap Forgot PIN to logout and reset.</string>
|
||||
<string name="settings_security_pin_code_use_biometrics_title">Enable biometrics</string>
|
||||
<string name="settings_security_pin_code_use_biometrics_summary_on">Enable device specific biometrics, like fingerprints and face recognition.</string>
|
||||
<string name="settings_security_pin_code_use_biometrics_summary_off">PIN code is the only way to unlock Element.</string>
|
||||
<string name="settings_security_pin_code_notifications_title">Show content in notifications</string>
|
||||
<string name="settings_security_pin_code_notifications_summary_on">Show details like room names and message content.</string>
|
||||
<string name="settings_security_pin_code_notifications_summary_off">Only display number of unread messages in a simple notification.</string>
|
||||
<string name="settings_security_pin_code_grace_period_title">Require PIN after 2 minutes</string>
|
||||
<string name="settings_security_pin_code_grace_period_summary_on">PIN code is required after 2 minutes of not using Element.</string>
|
||||
<string name="settings_security_pin_code_grace_period_summary_off">PIN code is required every time you open Element.</string>
|
||||
<string name="auth_pin_confirm_to_disable_title">Confirm PIN to disable PIN</string>
|
||||
<string name="error_opening_banned_room">Can\'t open a room where you are banned from.</string>
|
||||
<string name="room_error_not_found">Can\'t find this room. Make sure it exists.</string>
|
||||
|
@ -1,13 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="LoginContainer">
|
||||
<item name="android:paddingTop">32dp</item>
|
||||
<item name="android:paddingBottom">32dp</item>
|
||||
<item name="android:paddingStart">36dp</item>
|
||||
<item name="android:paddingEnd">36dp</item>
|
||||
</style>
|
||||
|
||||
<item name="loginLogo" type="id" />
|
||||
<item name="loginFormScrollView" type="id" />
|
||||
<item name="loginFormContainer" type="id" />
|
||||
|
34
vector/src/main/res/xml/vector_settings_pin.xml
Normal file
34
vector/src/main/res/xml/vector_settings_pin.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
|
||||
android:summary="@string/settings_security_pin_code_summary"
|
||||
android:title="@string/settings_security_pin_code_title" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:dependency="SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
|
||||
android:key="SETTINGS_SECURITY_USE_BIOMETRICS_FLAG"
|
||||
android:summaryOff="@string/settings_security_pin_code_use_biometrics_summary_off"
|
||||
android:summaryOn="@string/settings_security_pin_code_use_biometrics_summary_on"
|
||||
android:title="@string/settings_security_pin_code_use_biometrics_title" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:dependency="SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
|
||||
android:key="SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG"
|
||||
android:summaryOff="@string/settings_security_pin_code_notifications_summary_off"
|
||||
android:summaryOn="@string/settings_security_pin_code_notifications_summary_on"
|
||||
android:title="@string/settings_security_pin_code_notifications_title" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:dependency="SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
|
||||
android:key="SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG"
|
||||
android:summaryOff="@string/settings_security_pin_code_grace_period_summary_off"
|
||||
android:summaryOn="@string/settings_security_pin_code_grace_period_summary_on"
|
||||
android:title="@string/settings_security_pin_code_grace_period_title" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
@ -117,18 +117,18 @@
|
||||
|
||||
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/settings_other">
|
||||
|
||||
<im.vector.app.core.preference.VectorPreference
|
||||
android:key="SETTINGS_SECURITY_PIN"
|
||||
android:persistent="false"
|
||||
android:summary="@string/settings_security_application_protection_summary"
|
||||
android:title="@string/settings_security_application_protection_title" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_SECURITY_USE_FLAG_SECURE"
|
||||
android:summary="@string/settings_security_prevent_screenshots_summary"
|
||||
android:title="@string/settings_security_prevent_screenshots_title" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
|
||||
android:summary="@string/settings_security_pin_code_summary"
|
||||
android:title="@string/settings_security_pin_code_title" />
|
||||
|
||||
</im.vector.app.core.preference.VectorPreferenceCategory>
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
@ -17,7 +17,7 @@
|
||||
package im.vector.app.features.home
|
||||
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.utils.getColorFromUserId
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider.Companion.getColorFromUserId
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user