Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Cristian Greco 2020-09-29 17:40:55 +02:00
commit 68907ff6c0
43 changed files with 1989 additions and 366 deletions

View file

@ -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
View 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)
}
```

View file

@ -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()

View file

@ -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,

View file

@ -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

View file

@ -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
View 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")

View file

@ -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'

View 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
}

View 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()))
}
}
}

View file

@ -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);
}
};
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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() {

View file

@ -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)

View file

@ -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()
}
}
}
}

View file

@ -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).

View file

@ -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)
}
}

View file

@ -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() {

View file

@ -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) {

View file

@ -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
}
}
}
}

View file

@ -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)
})

View file

@ -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))
)
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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))

View file

@ -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
}
}
}

View file

@ -18,6 +18,5 @@ package im.vector.app.features.pin
enum class PinMode {
CREATE,
DELETE,
AUTH
}

View file

@ -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()
}

View file

@ -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) }
}
}

View file

@ -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

View file

@ -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) }

View file

@ -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()
}
}
}

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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" />

View 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>

View file

@ -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>

View file

@ -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