Merge branch 'develop' into feature/ons/extend_user_agent
This commit is contained in:
commit
33eeb721bf
@ -1,4 +1,4 @@
|
|||||||
Changes in Element v1.5.0 (2022-09-21)
|
Changes in Element v1.5.0 (2022-09-23)
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
Features ✨
|
Features ✨
|
||||||
@ -33,8 +33,6 @@ Other changes
|
|||||||
- Exclude legacy android support annotation library ([#7140](https://github.com/vector-im/element-android/issues/7140))
|
- Exclude legacy android support annotation library ([#7140](https://github.com/vector-im/element-android/issues/7140))
|
||||||
- Pulling no longer hosted im.dlg:android-dialer directly into the repository and removing legacy support library usages ([#7142](https://github.com/vector-im/element-android/issues/7142))
|
- Pulling no longer hosted im.dlg:android-dialer directly into the repository and removing legacy support library usages ([#7142](https://github.com/vector-im/element-android/issues/7142))
|
||||||
- Fixing build cache misses when compiling the vector module ([#7157](https://github.com/vector-im/element-android/issues/7157))
|
- Fixing build cache misses when compiling the vector module ([#7157](https://github.com/vector-im/element-android/issues/7157))
|
||||||
- New App Layout is now enabled by default! Go to the Settings > Labs to toggle this ([#7166](https://github.com/vector-im/element-android/issues/7166))
|
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.4.36 (2022-09-10)
|
Changes in Element v1.4.36 (2022-09-10)
|
||||||
=======================================
|
=======================================
|
||||||
|
@ -1,10 +1,42 @@
|
|||||||
# Contributing code to Matrix
|
# Contributing to Element Android
|
||||||
|
|
||||||
|
<!--- TOC -->
|
||||||
|
|
||||||
|
* [Contributing code to Matrix](#contributing-code-to-matrix)
|
||||||
|
* [Android Studio settings](#android-studio-settings)
|
||||||
|
* [Template](#template)
|
||||||
|
* [Compilation](#compilation)
|
||||||
|
* [I want to help translating Element](#i-want-to-help-translating-element)
|
||||||
|
* [I want to submit a PR to fix an issue](#i-want-to-submit-a-pr-to-fix-an-issue)
|
||||||
|
* [Kotlin](#kotlin)
|
||||||
|
* [Changelog](#changelog)
|
||||||
|
* [Code quality](#code-quality)
|
||||||
|
* [Internal tool](#internal-tool)
|
||||||
|
* [ktlint](#ktlint)
|
||||||
|
* [lint](#lint)
|
||||||
|
* [Unit tests](#unit-tests)
|
||||||
|
* [Tests](#tests)
|
||||||
|
* [Internationalisation](#internationalisation)
|
||||||
|
* [Adding new string](#adding-new-string)
|
||||||
|
* [Plurals](#plurals)
|
||||||
|
* [Editing existing strings](#editing-existing-strings)
|
||||||
|
* [Removing existing strings](#removing-existing-strings)
|
||||||
|
* [Renaming string ids](#renaming-string-ids)
|
||||||
|
* [Reordering strings](#reordering-strings)
|
||||||
|
* [Accessibility](#accessibility)
|
||||||
|
* [Layout](#layout)
|
||||||
|
* [Authors](#authors)
|
||||||
|
* [Thanks](#thanks)
|
||||||
|
|
||||||
|
<!--- END -->
|
||||||
|
|
||||||
|
## Contributing code to Matrix
|
||||||
|
|
||||||
Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md
|
Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md
|
||||||
|
|
||||||
Element Android support can be found in this room: [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org).
|
Element Android support can be found in this room: [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org).
|
||||||
|
|
||||||
# Specific rules for Matrix Android projects
|
The rest of the document contains specific rules for Matrix Android projects
|
||||||
|
|
||||||
## Android Studio settings
|
## Android Studio settings
|
||||||
|
|
||||||
@ -120,17 +152,21 @@ You should consider adding Unit tests with your PR, and also integration tests (
|
|||||||
|
|
||||||
Translations are handled using an external tool: [Weblate](https://translate.element.io/projects/element-android/)
|
Translations are handled using an external tool: [Weblate](https://translate.element.io/projects/element-android/)
|
||||||
|
|
||||||
As a general rule, please never edit or add or remove translations to the project in a Pull Request. It can lead to merge conflict if the translations are also modified in Weblate side.
|
**As a general rule, please never edit or add or remove translations to the project in a Pull Request**. It can lead to merge conflict if the translations are also modified in Weblate side. Pull Request containing change(s) on the translation files cannot be merged.
|
||||||
|
|
||||||
#### Adding new string
|
#### Adding new string
|
||||||
|
|
||||||
When adding new string resources, please only add new entries in the file `value/strings.xml`. Translations will be added later by the community of translators using Weblate.
|
When adding new string resources, please only add new entries in the file `values/strings.xml` ([this file](./library/ui-strings/src/main/res/values/strings.xml)). Translations will be added later by the community of translators using Weblate.
|
||||||
|
|
||||||
The file `value/strings.xml` must only contain American English (U. S. English) values, as this is the default language of the Android operating system. So for instance, please use "color" instead of "colour". Element Android will still use the language set on the system by the user, like any other Android applications which provide translations. The system language can be any other English language variants, or any other languages. Note that this is also possible to override the system language using the Element Android in-app language settings.
|
The file `values/strings.xml` must only contain American English (U. S. English) values, as this is the default language of the Android operating system. So for instance, please use "color" instead of "colour". Element Android will still use the language set on the system by the user, like any other Android applications which provide translations. The system language can be any other English language variants, or any other languages. Note that this is also possible to override the system language using the Element Android in-app language settings.
|
||||||
|
|
||||||
New strings can be added anywhere in the file `value/strings.xml`, not necessarily at the end of the file. Generally, it's even better to add the new strings in some dedicated section per feature, and not at the end of the file, to avoid merge conflict between 2 PR adding strings at the end of the same file.
|
New strings can be added anywhere in the file `values/strings.xml`, not necessarily at the end of the file. Generally, it's even better to add the new strings in some dedicated section per feature, and not at the end of the file, to avoid merge conflict between 2 PR adding strings at the end of the same file.
|
||||||
|
|
||||||
Do not hesitate to use plurals when appropriate.
|
##### Plurals
|
||||||
|
|
||||||
|
Please use `plurals` resources when appropriate, and note that some languages have specific rules for `plurals`, so even if the string will always be at the plural form for English, please always create a `plurals` resource.
|
||||||
|
|
||||||
|
Specific plural forms can be found [here](https://unicode-org.github.io/cldr-staging/charts/37/supplemental/language_plural_rules.html).
|
||||||
|
|
||||||
#### Editing existing strings
|
#### Editing existing strings
|
||||||
|
|
||||||
@ -150,6 +186,23 @@ And add `tools:ignore="UnusedResources"` to the string, to let lint ignore that
|
|||||||
|
|
||||||
The string will be removed during the next sync with Weblate.
|
The string will be removed during the next sync with Weblate.
|
||||||
|
|
||||||
|
#### Renaming string ids
|
||||||
|
|
||||||
|
This is possible to rename ids of the String resources, but since translation files cannot be edited, add TODO in the main strings.xml file above the strings you want to rename.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- TODO Rename id to put_new_id_here -->
|
||||||
|
<string name="current_id">Hello Matrix world!</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
The string id(s) will be renamed during the next Weblate sync.
|
||||||
|
|
||||||
|
#### Reordering strings
|
||||||
|
|
||||||
|
To group strings per feature, or for any other reasons, it is possible to reorder string resources, but only in the [main strings.xml file](./library/ui-strings/src/main/res/values/strings.xml). ). We do not mind about ordering in the translation files, and anyway this is forbidden to edit manually the translation files.
|
||||||
|
|
||||||
|
It is also possible to add empty lines between string resources, and to add XML comments. Please note that the XML comment just above a String resource will also appear on Weblate and be visible to the translators.
|
||||||
|
|
||||||
### Accessibility
|
### Accessibility
|
||||||
|
|
||||||
Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`.
|
Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`.
|
||||||
|
@ -44,10 +44,12 @@ If you would like to receive releases more quickly (bearing in mind that they ma
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
|
Please refer to [CONTRIBUTING.md](./CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
|
||||||
|
|
||||||
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).
|
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).
|
||||||
|
|
||||||
|
Also [this documentation](./docs/_developer_onboarding.md) can hopefully help developers to start working on the project.
|
||||||
|
|
||||||
## Triaging issues
|
## Triaging issues
|
||||||
|
|
||||||
Issues are triaged by community members and the Android App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process).
|
Issues are triaged by community members and the Android App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process).
|
||||||
|
@ -71,6 +71,14 @@ allprojects {
|
|||||||
groups.mavenCentral.group.each { includeGroup it }
|
groups.mavenCentral.group.each { includeGroup it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// snapshots repository
|
||||||
|
maven {
|
||||||
|
url "https://oss.sonatype.org/content/repositories/snapshots"
|
||||||
|
content {
|
||||||
|
groups.snapshot.regex.each { includeGroupByRegex it }
|
||||||
|
groups.snapshot.group.each { includeGroup it }
|
||||||
|
}
|
||||||
|
}
|
||||||
maven {
|
maven {
|
||||||
url 'https://jitpack.io'
|
url 'https://jitpack.io'
|
||||||
content {
|
content {
|
||||||
|
1
changelog.d/6929.misc
Normal file
1
changelog.d/6929.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Target API 12 and compile with Android SDK 32.
|
1
changelog.d/7126.doc
Normal file
1
changelog.d/7126.doc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Draft onboarding documentation of the project at `./docs/_developer_onboarding.md`
|
1
changelog.d/7126.misc
Normal file
1
changelog.d/7126.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add support to `/devtools` command.
|
1
changelog.d/7159.misc
Normal file
1
changelog.d/7159.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix lint warning, and cleanup the code
|
1
changelog.d/7198.sdk
Normal file
1
changelog.d/7198.sdk
Normal file
@ -0,0 +1 @@
|
|||||||
|
Allow the sync timeout to be configured (mainly useful for testing)
|
1
changelog.d/7211.misc
Normal file
1
changelog.d/7211.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
CI: Prevent modification of translations by developer.
|
@ -1,20 +1,17 @@
|
|||||||
ext.versions = [
|
ext.versions = [
|
||||||
|
|
||||||
'minSdk' : 21,
|
'minSdk' : 21,
|
||||||
'compileSdk' : 31,
|
'compileSdk' : 32,
|
||||||
'targetSdk' : 31,
|
'targetSdk' : 32,
|
||||||
'sourceCompat' : JavaVersion.VERSION_11,
|
'sourceCompat' : JavaVersion.VERSION_11,
|
||||||
'targetCompat' : JavaVersion.VERSION_11,
|
'targetCompat' : JavaVersion.VERSION_11,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def gradle = "7.2.2"
|
||||||
// Pinned to 7.1.3 because of https://github.com/vector-im/element-android/issues/6142
|
|
||||||
// Please test carefully before upgrading again.
|
|
||||||
def gradle = "7.1.3"
|
|
||||||
// Ref: https://kotlinlang.org/releases.html
|
// Ref: https://kotlinlang.org/releases.html
|
||||||
def kotlin = "1.6.21"
|
def kotlin = "1.7.10"
|
||||||
def kotlinCoroutines = "1.6.4"
|
def kotlinCoroutines = "1.6.4"
|
||||||
def dagger = "2.42"
|
def dagger = "2.43.2"
|
||||||
def appDistribution = "16.0.0-beta04"
|
def appDistribution = "16.0.0-beta04"
|
||||||
def retrofit = "2.9.0"
|
def retrofit = "2.9.0"
|
||||||
def arrow = "0.8.2"
|
def arrow = "0.8.2"
|
||||||
@ -28,7 +25,9 @@ def mavericks = "2.7.0"
|
|||||||
def glide = "4.13.2"
|
def glide = "4.13.2"
|
||||||
def bigImageViewer = "1.8.1"
|
def bigImageViewer = "1.8.1"
|
||||||
def jjwt = "0.11.5"
|
def jjwt = "0.11.5"
|
||||||
def vanniktechEmoji = "0.15.0"
|
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
|
||||||
|
// the whole commit which set version 0.16.0-SNAPSHOT
|
||||||
|
def vanniktechEmoji = "0.16.0-SNAPSHOT"
|
||||||
|
|
||||||
def fragment = "1.5.2"
|
def fragment = "1.5.2"
|
||||||
|
|
||||||
@ -51,7 +50,7 @@ ext.libs = [
|
|||||||
],
|
],
|
||||||
androidx : [
|
androidx : [
|
||||||
'activity' : "androidx.activity:activity:1.5.1",
|
'activity' : "androidx.activity:activity:1.5.1",
|
||||||
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
|
'appCompat' : "androidx.appcompat:appcompat:1.5.1",
|
||||||
'biometric' : "androidx.biometric:biometric:1.1.0",
|
'biometric' : "androidx.biometric:biometric:1.1.0",
|
||||||
'core' : "androidx.core:core-ktx:1.8.0",
|
'core' : "androidx.core:core-ktx:1.8.0",
|
||||||
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
||||||
|
@ -38,6 +38,13 @@ ext.groups = [
|
|||||||
'com.google.testing.platform',
|
'com.google.testing.platform',
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
snapshot: [
|
||||||
|
regex: [
|
||||||
|
],
|
||||||
|
group: [
|
||||||
|
'com.vanniktech',
|
||||||
|
]
|
||||||
|
],
|
||||||
mavenCentral: [
|
mavenCentral: [
|
||||||
regex: [
|
regex: [
|
||||||
],
|
],
|
||||||
@ -118,7 +125,7 @@ ext.groups = [
|
|||||||
'com.sun.xml.bind.mvn',
|
'com.sun.xml.bind.mvn',
|
||||||
'com.sun.xml.fastinfoset',
|
'com.sun.xml.fastinfoset',
|
||||||
'com.thoughtworks.qdox',
|
'com.thoughtworks.qdox',
|
||||||
'com.vanniktech',
|
// 'com.vanniktech',
|
||||||
'commons-cli',
|
'commons-cli',
|
||||||
'commons-codec',
|
'commons-codec',
|
||||||
'commons-io',
|
'commons-io',
|
||||||
|
259
docs/_developer_onboarding.md
Normal file
259
docs/_developer_onboarding.md
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
# Developer on boarding
|
||||||
|
|
||||||
|
<!--- TOC -->
|
||||||
|
|
||||||
|
* [Introduction](#introduction)
|
||||||
|
* [Quick introduction to Matrix](#quick-introduction-to-matrix)
|
||||||
|
* [Matrix data](#matrix-data)
|
||||||
|
* [Room](#room)
|
||||||
|
* [Event](#event)
|
||||||
|
* [Sync](#sync)
|
||||||
|
* [Glossary about syncs](#glossary-about-syncs)
|
||||||
|
* [The Android project](#the-android-project)
|
||||||
|
* [Matrix SDK](#matrix-sdk)
|
||||||
|
* [Application](#application)
|
||||||
|
* [MvRx](#mvrx)
|
||||||
|
* [Behavior](#behavior)
|
||||||
|
* [Epoxy](#epoxy)
|
||||||
|
* [Other frameworks](#other-frameworks)
|
||||||
|
* [Push](#push)
|
||||||
|
* [Dependencies management](#dependencies-management)
|
||||||
|
* [Test](#test)
|
||||||
|
* [Other points](#other-points)
|
||||||
|
* [Logging](#logging)
|
||||||
|
* [Rageshake](#rageshake)
|
||||||
|
* [Tips](#tips)
|
||||||
|
* [Happy coding!](#happy-coding)
|
||||||
|
|
||||||
|
<!--- END -->
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This doc is a quick introduction about the project and its architecture.
|
||||||
|
|
||||||
|
It's aim is to help new developers to understand the overall project and where to start developing.
|
||||||
|
|
||||||
|
Other useful documentation:
|
||||||
|
- all the docs in this folder!
|
||||||
|
- the [contributing doc](../CONTRIBUTING.md), that you should also read carefully.
|
||||||
|
|
||||||
|
### Quick introduction to Matrix
|
||||||
|
|
||||||
|
Matrix website: [matrix.org](https://matrix.org), [discover page](https://matrix.org/discover).
|
||||||
|
*Note*: Matrix.org is also hosting a homeserver ([.well-known file](https://matrix.org/.well-known/matrix/client)).
|
||||||
|
The reference homeserver (this is how Matrix servers are called) implementation is [Synapse](https://github.com/matrix-org/synapse/). But other implementations exist. The Matrix specification is here to ensure that any Matrix client, such as Element Android and its SDK can talk to any Matrix server.
|
||||||
|
|
||||||
|
Have a quick look to the client-server API documentation: [Client-server documentation](https://spec.matrix.org/v1.3/client-server-api/). Other network API exist, the list is here: (https://spec.matrix.org/latest/)
|
||||||
|
|
||||||
|
Matrix is an open source protocol. Change are possible and are tracked using [this GitHub repository](https://github.com/matrix-org/matrix-doc/). Changes to the protocol are called MSC: Matrix Spec Change. These are PullRequest to this project.
|
||||||
|
|
||||||
|
Matrix object are Json data. Unstable prefixes must be used for Json keys when the MSC is not merged (i.e. accepted).
|
||||||
|
|
||||||
|
#### Matrix data
|
||||||
|
|
||||||
|
There are many object and data in the Matrix worlds. Let's focus on the most important and used, `Room` and `Event`
|
||||||
|
|
||||||
|
##### Room
|
||||||
|
|
||||||
|
`Room` is a place which contains ordered `Event`s. They are identified with their `room_id`. Nearly all the data are stored in rooms, and shared using homeserver to all the Room Member.
|
||||||
|
|
||||||
|
*Note*: Spaces are also Rooms with a different `type`.
|
||||||
|
|
||||||
|
##### Event
|
||||||
|
|
||||||
|
`Events` are items of a Room, where data is embedded.
|
||||||
|
|
||||||
|
There are 2 types of Room Event:
|
||||||
|
|
||||||
|
- Regular Events: contain useful content for the user (message, image, etc.), but are not necessarily displayed as this in the timeline (reaction, message edition, call signaling).
|
||||||
|
- State Events: contain the state of the Room (name, topic, etc.). They have a non null value for the key `state_key`.
|
||||||
|
|
||||||
|
Also all the Room Member details are in State Events: one State Event per member. In this case, the `state_key` is the matrixId (= userId).
|
||||||
|
|
||||||
|
Important Fields of an Event:
|
||||||
|
- `event_id`: unique across the Matrix universe;
|
||||||
|
- `room_id`: the room the Event belongs to;
|
||||||
|
- `type`: describe what the Event contain, especially in the `content` section, and how the SDK should handle this Event;
|
||||||
|
- `content`: dynamic Event data; depends on the `type`.
|
||||||
|
|
||||||
|
So we have a triple `event_id`, `type`, `state_key` which uniquely defines an Event.
|
||||||
|
|
||||||
|
#### Sync
|
||||||
|
|
||||||
|
The `Sync` is a way for the Matrix client to be up to date regarding the user data hosted by the server. All the Events are coming through the sync response. More details can be found here: [spec.matrix.org/v1.3/client-server-api/#syncing](https://spec.matrix.org/v1.3/client-server-api/#syncing)
|
||||||
|
When the application is in foreground, this is a looping request. We are using Https requests, which offer the advantage to be compatible with any homeserver. A sync token is used as request parameter, to let the server know what the client knows.
|
||||||
|
The `SyncThread` is responsible to manage the sync request loop.
|
||||||
|
|
||||||
|
When the application is in background, a Push will trigger a sync request.
|
||||||
|
|
||||||
|
##### Glossary about syncs
|
||||||
|
|
||||||
|
- **initial sync**: a sync request without a token. This is the first request a client perform after login or after a clear cache. The server will include in the response all your rooms with the full state (all the room membership Event will not be present), with the latest messages for each room. We are in the process to replace this by version 3: sliding sync. All data are inserted to the Database (currently [Realm](https://www.mongodb.com/docs/realm/sdk/java/)).
|
||||||
|
- **incremental sync**: sync request with a token.
|
||||||
|
- **gappy sync**: sync request where all the new Events are not returned for one or several Rooms. Also called `limited sync`. It can be limited per Room. To get all the missing Events, a Room pagination API has to be called.
|
||||||
|
- **sync token**: `next_batch` value in the previous sync response. Will be provided as the `since` parameter for the next sync request.
|
||||||
|
|
||||||
|
### The Android project
|
||||||
|
|
||||||
|
The project should compile out of the box.
|
||||||
|
|
||||||
|
The project is split into several modules. The main ones are:
|
||||||
|
For the app
|
||||||
|
- `vector-app`: application entry point;
|
||||||
|
- `vector`: legacy application, but now a library. In the process of being split into several modules;
|
||||||
|
- `vector-config`: this is where all the configuration of the application should occurs. Should because we are in the process of migrating all the configuration here;
|
||||||
|
- `library/ui-strings`: this is where all the string resources are stored. Please refer to [contributing doc](../CONTRIBUTING.md) to know how to make change on this module;
|
||||||
|
- `library/ui-styles`: this is where the Android styles are defined.
|
||||||
|
|
||||||
|
For the SDK
|
||||||
|
- `matrix-sdk-android`: the main SDK module. The sources are in this project, but are also exported to [its own project](https://github.com/matrix-org/matrix-android-sdk2). All the PRs and issues related to the SDK take place in the Element Android project;
|
||||||
|
- `matrix-sdk-android-flow`: contains some wrapper to expose `Flow` to the application.
|
||||||
|
|
||||||
|
### Matrix SDK
|
||||||
|
|
||||||
|
SDK exposes `Services` to the client application. `Services` are public interface, and are defined in this parent package: `org.matrix.android.sdk.api`. Default implementation are internal to the SDK, in this parent package: `org.matrix.android.sdk.internal`. Note that you also have to declare the classes as `internal` when adding classes to the `org.matrix.android.sdk.internal` package.
|
||||||
|
|
||||||
|
Interface allows us to replace the implementation for testing purpose.
|
||||||
|
|
||||||
|
A generated documentation of the SDK is available [here](https://matrix-org.github.io/matrix-android-sdk2/). Updated after each release. Please ensure that the documentation (KDoc) of all the SDK Services is up to date, and is clear for a SDK user.
|
||||||
|
The SDK generated documentation also contains information about the entry points of the SDK.
|
||||||
|
|
||||||
|
[Dagger](https://dagger.dev/) is used to inject all the dependencies to the SDK classes.
|
||||||
|
|
||||||
|
SDK is exposing data as `LiveData`, but we are progressively migrating to `Flow`. Database is the source of truth.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- Client send an Event using the `SendService`;
|
||||||
|
- At the end a `SendEvent` task is used;
|
||||||
|
- Retrofit API is used to send data to the server;
|
||||||
|
- Goes to the server, which returns only the `event_id`;
|
||||||
|
- The `Event` is coming back from the `sync` response with eventually extra added data.
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
This is the UI part of the project.
|
||||||
|
|
||||||
|
There are two variants of the application: `Gplay` and `Fdroid`.
|
||||||
|
|
||||||
|
The main difference is about using Firebase on `Gplay` variant, to have Push from Google Services. `FDroid` variant cannot contain closed source dependency.
|
||||||
|
|
||||||
|
`Fdroid` is using background polling to lack the missing of Pushed. Now a solution using UnifiedPush has ben added to the project. See refer to [the dedicated documentation](./unifiedpush.md) for more details.
|
||||||
|
|
||||||
|
#### MvRx
|
||||||
|
|
||||||
|
[Maverick](https://airbnb.io/mavericks/#/README) (or MvRx) is an Android MVI framework that helps to develop Reactive application on Android.
|
||||||
|
|
||||||
|
- Activity: holder for Fragment. See the parent [VectorBaseActivity](../vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt);
|
||||||
|
- Fragment: manage screen of the application. See the parent [VectorBaseFragment](../vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt);
|
||||||
|
- BottomSheet: see the parent [VectorBaseBottomSheetDialogFragment](../vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt);
|
||||||
|
- ViewModel: this is where the logic is placed. All our ViewModel has a `handle()` which takes action as parameter. See the parent [VectorViewModel](../vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt);
|
||||||
|
- VectorSharedActionViewModel: Specific ViewModel that can be used to communicate between Fragment(s) and the host Activity. See the parent [VectorSharedActionViewModel](../vector/src/main/java/im/vector/app/core/platform/VectorSharedActionViewModel.kt);
|
||||||
|
- ViewState: this are `data class`, and this represent the state of the View. Has to be copied and set to be updated. Fragment will update the UI regarding the current state (`invalidate()` method). `Async` class from MvRx can be used in the ViewState, especially for asynchronous data loading. Nullability can also be used for optional data. ViewStates have to implement `MavericksState`;
|
||||||
|
- ViewEvents: useful when the ViewModel asks the View to trigger a specific action: navigation, show dialog, etc. See the parent [VectorViewEvents](../vector/src/main/java/im/vector/app/core/platform/VectorViewEvents.kt);
|
||||||
|
- ViewAction (`VectorViewModelAction`): useful when the UI (generally the Fragment) asks the ViewModel to do something. See the parent [VectorViewModelAction](../vector/src/main/java/im/vector/app/core/platform/VectorViewModelAction.kt);
|
||||||
|
- Controller: see the `Epoxy` section just below.
|
||||||
|
|
||||||
|
##### Behavior
|
||||||
|
|
||||||
|
Fragment asks the ViewModel to perform an action (coming from the user, but not necessarily. ViewModel can then talk to the SDK, updates the state once or several times. Fragment update the UI regarding the new state.
|
||||||
|
|
||||||
|
When ViewModel is instantiated, it can subscribe using the SDK Services to get live state of the data.
|
||||||
|
|
||||||
|
`invalidate()` has to be used by default, but it's possible to listen to specific member(s) of the `ViewState` using `onEach`. TODO Add an example.
|
||||||
|
`awaitState()` method
|
||||||
|
|
||||||
|
#### Epoxy
|
||||||
|
|
||||||
|
[Epoxy](https://github.com/airbnb/epoxy) is an Android library for building complex screens in a RecyclerView. Please read [the introduction](https://github.com/airbnb/epoxy#epoxy).
|
||||||
|
|
||||||
|
- Controller declares items of the RecyclerView. Controller is injected in the Fragment. Controller extends `EpoxyController`, or one of its subclass, especially `TypedEpoxyController`;
|
||||||
|
- Fragment gives the state to the controller using `setData`;
|
||||||
|
- `buildModels` will be called by the framework;
|
||||||
|
- Controller will create ordered Items.
|
||||||
|
|
||||||
|
Epoxy does the diffing, and handle many other thing for us, like handling item type, etc.
|
||||||
|
|
||||||
|
See for instance the controller [AccountDataEpoxyController](../vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt)) for a simple example.
|
||||||
|
|
||||||
|
Warning: do not use twice the same item `id` or it will crash.
|
||||||
|
|
||||||
|
#### Other frameworks
|
||||||
|
|
||||||
|
- Dependency injection is managed by [Dagger](https://dagger.dev/) (SDK) and [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) (App);
|
||||||
|
- [Retrofit](https://square.github.io/retrofit/) and [OkHttp3](https://square.github.io/okhttp/): network requests;
|
||||||
|
- [Moshi](https://github.com/square/moshi) is used to parse and serialize Json object;
|
||||||
|
|
||||||
|
### Push
|
||||||
|
|
||||||
|
Please see the dedicated documentation for more details.
|
||||||
|
|
||||||
|
This is the classical scenario:
|
||||||
|
|
||||||
|
- App receives a Push. Note: Push is ignored if app is in foreground;
|
||||||
|
- App asks the SDK to load Event data (fastlane mode). We have a change to get the data faster and display the notification faster;
|
||||||
|
- App asks the SDK to perform a sync request.
|
||||||
|
|
||||||
|
### Dependencies management
|
||||||
|
|
||||||
|
All the dependencies are declared in `build.gradle` files. But some versions are declared in [this dedicated file](../dependencies.gradle).
|
||||||
|
|
||||||
|
When adding a new dependency, you will have to update the file [dependencies_groups.gradle](../dependencies_groups.gradle) to allow the dependency to be downloaded from the artifact repository. Sometimes sub-dependencies need to be added too, until the project can compile.
|
||||||
|
|
||||||
|
[Dependabot](https://github.com/dependabot) is set up on the project. This tool will automatically create Pull Request to upgrade our dependencies one by one.
|
||||||
|
dependencies_group, gradle files, Dependabot, etc.
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Please refer to [this dedicated document](./ui-tests.md).
|
||||||
|
|
||||||
|
TODO add link to the dedicated screenshot test documentation
|
||||||
|
|
||||||
|
### Other points
|
||||||
|
|
||||||
|
#### Logging
|
||||||
|
|
||||||
|
**Important warning: ** NEVER log private user data, or use the flag `LOG_PRIVATE_DATA`. Be very careful when logging `data class`, all the content will be output!
|
||||||
|
|
||||||
|
[Timber](https://github.com/JakeWharton/timber) is used to log data to logcat. We do not use directly the `Log` class. If possible please use a tag, as per
|
||||||
|
|
||||||
|
````kotlin
|
||||||
|
Timber.tag(loggerTag.value).d("my log")
|
||||||
|
````
|
||||||
|
|
||||||
|
because automatic tag (= class name) will not be available on the release version.
|
||||||
|
|
||||||
|
Also generally it is recommended to provide the `Throwable` to the Timber log functions.
|
||||||
|
|
||||||
|
Last point, not that `Timber.v` function may have no effect on some devices. Prefer using `Timber.d` and up.
|
||||||
|
|
||||||
|
#### Rageshake
|
||||||
|
|
||||||
|
Rageshake is a feature to send bug report directly from the application. Just shake your phone and you will be prompted to send a bug report.
|
||||||
|
|
||||||
|
Bug report can contain:
|
||||||
|
- a screenshot of the current application state
|
||||||
|
- the application logs from up to 15 application starts
|
||||||
|
- the logcat logs
|
||||||
|
- the key share history (crypto data)
|
||||||
|
|
||||||
|
The data will be sent to an internal server, which is not publicly accessible. A GitHub issue will also be created to a private GitHub repository.
|
||||||
|
|
||||||
|
Rageshake can be very useful to get logs from a release version of the application.
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
- Element Android has a `developer mode` in the `Settings/Advanced settings`. Other useful options are available here;
|
||||||
|
- Show hidden Events can also help to debug feature. When developer mode is enabled, it is possible to view the source (= the Json content) of any Events;
|
||||||
|
- Type `/devtools` in a Room composer to access a developer menu. There are some other entry points. Developer mode has to be enabled;
|
||||||
|
- Hidden debug menu: when developer mode is enabled and on debug build, there are some extra screens that can be accessible using the green wheel. In those screens, it will be possible to toggle some feature flags;
|
||||||
|
- Using logcat, filtering with `onResume` can help you to understand what screen are currently displayed on your device. Searching for string displayed on the screen can also help to find the running code in the codebase.
|
||||||
|
- When this is possible, prefer using `sealed interface` instead of `sealed class`;
|
||||||
|
- When writing temporary code, using the string "DO NOT COMMIT" in a comment can help to avoid committing things by mistake. If committed and pushed, the CI will detect this String and will warn the user about it.
|
||||||
|
|
||||||
|
## Happy coding!
|
||||||
|
|
||||||
|
The team is here to support you, feel free to ask anything to other developers.
|
||||||
|
|
||||||
|
Also please feel to update this documentation, if incomplete/wrong/obsolete/etc.
|
||||||
|
|
||||||
|
**Thanks!**
|
@ -28,6 +28,7 @@ Here are the checks that Danger does so far:
|
|||||||
- PR with change on layout should include screenshot in the description
|
- PR with change on layout should include screenshot in the description
|
||||||
- PR which adds png file warn about the usage of vector drawables
|
- PR which adds png file warn about the usage of vector drawables
|
||||||
- non draft PR should have a reviewer
|
- non draft PR should have a reviewer
|
||||||
|
- files containing translations are not modified by developers
|
||||||
|
|
||||||
### Quality check
|
### Quality check
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
Main changes in this version: New App Layout and Deferred DM enabled by default.
|
Main changes in this version: Deferred DM enabled by default.
|
||||||
Full changelog: https://github.com/vector-im/element-android/releases
|
Full changelog: https://github.com/vector-im/element-android/releases
|
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
package im.vector.lib.attachmentviewer
|
package im.vector.lib.attachmentviewer
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -136,7 +135,6 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun setDecorViewFullScreen() {
|
private fun setDecorViewFullScreen() {
|
||||||
// This is important for the dispatchTouchEvent, if not we must correct
|
// This is important for the dispatchTouchEvent, if not we must correct
|
||||||
// the touch coordinates
|
// the touch coordinates
|
||||||
@ -144,22 +142,20 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
window.setDecorFitsSystemWindows(false)
|
window.setDecorFitsSystemWindows(false)
|
||||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
} else {
|
|
||||||
@SuppressLint("WrongConstant")
|
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
|
||||||
}
|
|
||||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||||
// new API instead of FLAG_TRANSLUCENT_NAVIGATION
|
// new API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||||
} else {
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.decorView.systemUiVisibility = (
|
window.decorView.systemUiVisibility = (
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
or View.SYSTEM_UI_FLAG_IMMERSIVE)
|
or View.SYSTEM_UI_FLAG_IMMERSIVE)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -344,7 +340,6 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||||||
?.handleCommand(commands)
|
?.handleCommand(commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun hideSystemUI() {
|
private fun hideSystemUI() {
|
||||||
systemUiVisibility = false
|
systemUiVisibility = false
|
||||||
// Enables regular immersive mode.
|
// Enables regular immersive mode.
|
||||||
@ -356,17 +351,13 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||||||
// new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
// new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
|
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
|
||||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
} else {
|
|
||||||
@SuppressLint("WrongConstant")
|
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
|
||||||
}
|
|
||||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||||
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||||
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||||
} else {
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
// Set the content to appear under the system bars so that the
|
// Set the content to appear under the system bars so that the
|
||||||
// content doesn't resize when the system bars hide and show.
|
// content doesn't resize when the system bars hide and show.
|
||||||
@ -381,13 +372,13 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||||||
|
|
||||||
// Shows the system bars by removing all the flags
|
// Shows the system bars by removing all the flags
|
||||||
// except for the ones that make the content appear under the system bars.
|
// except for the ones that make the content appear under the system bars.
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun showSystemUI() {
|
private fun showSystemUI() {
|
||||||
systemUiVisibility = true
|
systemUiVisibility = true
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
window.setDecorFitsSystemWindows(false)
|
window.setDecorFitsSystemWindows(false)
|
||||||
} else {
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||||
|
@ -1404,6 +1404,7 @@
|
|||||||
<string name="command_description_avatar_for_room">Changes your avatar in this current room only</string>
|
<string name="command_description_avatar_for_room">Changes your avatar in this current room only</string>
|
||||||
<string name="command_description_markdown">On/Off markdown</string>
|
<string name="command_description_markdown">On/Off markdown</string>
|
||||||
<string name="command_description_clear_scalar_token">To fix Matrix Apps management</string>
|
<string name="command_description_clear_scalar_token">To fix Matrix Apps management</string>
|
||||||
|
<string name="command_description_devtools">Open the developer tools screen</string>
|
||||||
<string name="command_description_whois">Displays information about a user</string>
|
<string name="command_description_whois">Displays information about a user</string>
|
||||||
|
|
||||||
<string name="markdown_has_been_enabled">Markdown has been enabled.</string>
|
<string name="markdown_has_been_enabled">Markdown has been enabled.</string>
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
android:id="@+id/menuDebug2"
|
android:id="@+id/menuDebug2"
|
||||||
android:icon="@drawable/ic_debug_icon"
|
android:icon="@drawable/ic_debug_icon"
|
||||||
android:title="Send"
|
android:title="Send"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always"
|
||||||
|
tools:ignore="AlwaysShowAction" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
@ -36,6 +36,7 @@ import org.junit.Assert.assertNotNull
|
|||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
|
import org.matrix.android.sdk.api.SyncConfig
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -103,7 +104,8 @@ class CommonTestHelper internal constructor(context: Context) {
|
|||||||
context,
|
context,
|
||||||
MatrixConfiguration(
|
MatrixConfiguration(
|
||||||
applicationFlavor = "TestFlavor",
|
applicationFlavor = "TestFlavor",
|
||||||
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
|
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(),
|
||||||
|
syncConfig = SyncConfig(longPollTimeout = 5_000L),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -70,4 +70,8 @@ data class MatrixConfiguration(
|
|||||||
* List of network interceptors, they will be added when building an OkHttp client.
|
* List of network interceptors, they will be added when building an OkHttp client.
|
||||||
*/
|
*/
|
||||||
val networkInterceptors: List<Interceptor> = emptyList(),
|
val networkInterceptors: List<Interceptor> = emptyList(),
|
||||||
|
/**
|
||||||
|
* Sync configuration.
|
||||||
|
*/
|
||||||
|
val syncConfig: SyncConfig = SyncConfig(),
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api
|
||||||
|
|
||||||
|
data class SyncConfig(
|
||||||
|
/**
|
||||||
|
* Time to keep sync connection alive for before making another request in milliseconds.
|
||||||
|
*/
|
||||||
|
val longPollTimeout: Long = 30_000L,
|
||||||
|
)
|
@ -131,11 +131,10 @@ class SecretStoringUtils @Inject constructor(
|
|||||||
*
|
*
|
||||||
* The secret is encrypted using the following method: AES/GCM/NoPadding
|
* The secret is encrypted using the following method: AES/GCM/NoPadding
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray {
|
fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray {
|
||||||
return when {
|
return when {
|
||||||
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias)
|
buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> encryptBytesM(secret, keyAlias)
|
||||||
else -> encryptBytes(secret, keyAlias)
|
else -> encryptBytes(secret, keyAlias)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,10 +155,9 @@ class SecretStoringUtils @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) {
|
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) {
|
||||||
when {
|
when {
|
||||||
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
|
buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> saveSecureObjectM(keyAlias, output, any)
|
||||||
else -> saveSecureObject(keyAlias, output, any)
|
else -> saveSecureObject(keyAlias, output, any)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,7 +187,6 @@ class SecretStoringUtils @Inject constructor(
|
|||||||
return cipher
|
return cipher
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
|
private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
|
||||||
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.util
|
package org.matrix.android.sdk.api.util
|
||||||
|
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
|
|
||||||
interface BuildVersionSdkIntProvider {
|
interface BuildVersionSdkIntProvider {
|
||||||
/**
|
/**
|
||||||
* Return the current version of the Android SDK.
|
* Return the current version of the Android SDK.
|
||||||
@ -26,9 +28,13 @@ interface BuildVersionSdkIntProvider {
|
|||||||
* Checks the if the current OS version is equal or greater than [version].
|
* Checks the if the current OS version is equal or greater than [version].
|
||||||
* @return A `non-null` result if true, `null` otherwise.
|
* @return A `non-null` result if true, `null` otherwise.
|
||||||
*/
|
*/
|
||||||
|
@ChecksSdkIntAtLeast(parameter = 0, lambda = 1)
|
||||||
fun <T> whenAtLeast(version: Int, result: () -> T): T? {
|
fun <T> whenAtLeast(version: Int, result: () -> T): T? {
|
||||||
return if (get() >= version) {
|
return if (get() >= version) {
|
||||||
result()
|
result()
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ChecksSdkIntAtLeast(parameter = 0)
|
||||||
|
fun isAtLeast(version: Int) = get() >= version
|
||||||
}
|
}
|
||||||
|
@ -1,235 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.session.room.send.queue
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
|
||||||
import org.matrix.android.sdk.api.auth.data.sessionId
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
|
||||||
import org.matrix.android.sdk.api.failure.isLimitExceededError
|
|
||||||
import org.matrix.android.sdk.api.failure.isTokenError
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Timer
|
|
||||||
import java.util.TimerTask
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple ever running thread unique for that session responsible of sending events in order.
|
|
||||||
* Each send is retried 3 times, if there is no network (e.g if cannot ping homeserver) it will wait and
|
|
||||||
* periodically test reachability before resume (does not count as a retry)
|
|
||||||
*
|
|
||||||
* If the app is killed before all event were sent, on next wakeup the scheduled events will be re posted
|
|
||||||
*/
|
|
||||||
@Deprecated("You should know use EventSenderProcessorCoroutine instead")
|
|
||||||
@SessionScope
|
|
||||||
internal class EventSenderProcessorThread @Inject constructor(
|
|
||||||
private val cryptoService: CryptoService,
|
|
||||||
private val sessionParams: SessionParams,
|
|
||||||
private val queuedTaskFactory: QueuedTaskFactory,
|
|
||||||
private val taskExecutor: TaskExecutor,
|
|
||||||
private val memento: QueueMemento
|
|
||||||
) : Thread("Matrix-SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}"), EventSenderProcessor {
|
|
||||||
|
|
||||||
private fun markAsManaged(task: QueuedTask) {
|
|
||||||
memento.track(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun markAsFinished(task: QueuedTask) {
|
|
||||||
memento.unTrack(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionStarted(session: Session) {
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSessionStopped(session: Session) {
|
|
||||||
interrupt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
|
||||||
super.start()
|
|
||||||
// We should check for sending events not handled because app was killed
|
|
||||||
// But we should be careful of only took those that was submitted to us, because if it's
|
|
||||||
// for example it's a media event it is handled by some worker and he will handle it
|
|
||||||
// This is a bit fragile :/
|
|
||||||
// also some events cannot be retried manually by users, e.g reactions
|
|
||||||
// they were previously relying on workers to do the work :/ and was expected to always finally succeed
|
|
||||||
// Also some echos are not to be resent like redaction echos (fake event created for aggregation)
|
|
||||||
|
|
||||||
tryOrNull {
|
|
||||||
taskExecutor.executorScope.launch {
|
|
||||||
Timber.d("## Send relaunched pending events on restart")
|
|
||||||
memento.restoreTasks(this@EventSenderProcessorThread)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API
|
|
||||||
override fun postEvent(event: Event): Cancelable {
|
|
||||||
return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun postEvent(event: Event, encrypt: Boolean): Cancelable {
|
|
||||||
val task = queuedTaskFactory.createSendTask(event, encrypt)
|
|
||||||
return postTask(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable {
|
|
||||||
return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable {
|
|
||||||
val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason)
|
|
||||||
return postTask(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun postTask(task: QueuedTask): Cancelable {
|
|
||||||
// non blocking add to queue
|
|
||||||
sendingQueue.add(task)
|
|
||||||
markAsManaged(task)
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel(eventId: String, roomId: String) {
|
|
||||||
(currentTask as? SendEventQueuedTask)
|
|
||||||
?.takeIf { it.event.eventId == eventId && it.event.roomId == roomId }
|
|
||||||
?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val RETRY_WAIT_TIME_MS = 10_000L
|
|
||||||
}
|
|
||||||
|
|
||||||
private var currentTask: QueuedTask? = null
|
|
||||||
|
|
||||||
private var sendingQueue = LinkedBlockingQueue<QueuedTask>()
|
|
||||||
|
|
||||||
private var networkAvailableLock = Object()
|
|
||||||
private var canReachServer = true
|
|
||||||
private var retryNoNetworkTask: TimerTask? = null
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
Timber.v("## SendThread started")
|
|
||||||
try {
|
|
||||||
while (!isInterrupted) {
|
|
||||||
Timber.v("## SendThread wait for task to process")
|
|
||||||
val task = sendingQueue.take()
|
|
||||||
.also { currentTask = it }
|
|
||||||
Timber.v("## SendThread Found task to process $task")
|
|
||||||
|
|
||||||
if (task.isCancelled()) {
|
|
||||||
Timber.v("## SendThread send cancelled for $task")
|
|
||||||
// we do not execute this one
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// we check for network connectivity
|
|
||||||
while (!canReachServer) {
|
|
||||||
Timber.v("## SendThread cannot reach server")
|
|
||||||
// schedule to retry
|
|
||||||
waitForNetwork()
|
|
||||||
// if thread as been killed meanwhile
|
|
||||||
// if (state == State.KILLING) break
|
|
||||||
}
|
|
||||||
Timber.v("## Server is Reachable")
|
|
||||||
// so network is available
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
retryLoop@ while (task.retryCount.get() < 3) {
|
|
||||||
try {
|
|
||||||
// SendPerformanceProfiler.startStage(task.event.eventId!!, SendPerformanceProfiler.Stages.SEND_WORKER)
|
|
||||||
Timber.v("## SendThread retryLoop for $task retryCount ${task.retryCount}")
|
|
||||||
task.execute()
|
|
||||||
// sendEventTask.execute(SendEventTask.Params(task.event, task.encrypt, cryptoService))
|
|
||||||
// SendPerformanceProfiler.stopStage(task.event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
|
|
||||||
break@retryLoop
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
when {
|
|
||||||
exception is IOException || exception is Failure.NetworkConnection -> {
|
|
||||||
canReachServer = false
|
|
||||||
if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed()
|
|
||||||
while (!canReachServer) {
|
|
||||||
Timber.v("## SendThread retryLoop cannot reach server")
|
|
||||||
// schedule to retry
|
|
||||||
waitForNetwork()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(exception.isLimitExceededError()) -> {
|
|
||||||
if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed()
|
|
||||||
Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}")
|
|
||||||
// wait a bit
|
|
||||||
// Todo if its a quota exception can we get timout?
|
|
||||||
sleep(3_000)
|
|
||||||
continue@retryLoop
|
|
||||||
}
|
|
||||||
exception.isTokenError() -> {
|
|
||||||
Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt")
|
|
||||||
// we can exit the loop
|
|
||||||
task.onTaskFailed()
|
|
||||||
throw InterruptedException()
|
|
||||||
}
|
|
||||||
exception is CancellationException -> {
|
|
||||||
Timber.v("## SendThread task has been cancelled")
|
|
||||||
break@retryLoop
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
|
|
||||||
// this task is in error, check next one?
|
|
||||||
task.onTaskFailed()
|
|
||||||
break@retryLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
markAsFinished(task)
|
|
||||||
}
|
|
||||||
} catch (interruptionException: InterruptedException) {
|
|
||||||
// will be thrown is thread is interrupted while seeping
|
|
||||||
interrupt()
|
|
||||||
Timber.v("## InterruptedException!! ${interruptionException.localizedMessage}")
|
|
||||||
}
|
|
||||||
// state = State.KILLED
|
|
||||||
// is this needed?
|
|
||||||
retryNoNetworkTask?.cancel()
|
|
||||||
Timber.w("## SendThread finished")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun waitForNetwork() {
|
|
||||||
retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) {
|
|
||||||
synchronized(networkAvailableLock) {
|
|
||||||
canReachServer = HomeServerAvailabilityChecker(sessionParams).check().also {
|
|
||||||
Timber.v("## SendThread checkHostAvailable $it")
|
|
||||||
}
|
|
||||||
networkAvailableLock.notify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
synchronized(networkAvailableLock) { networkAvailableLock.wait() }
|
|
||||||
}
|
|
||||||
}
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.isTokenError
|
import org.matrix.android.sdk.api.failure.isTokenError
|
||||||
@ -52,7 +53,6 @@ import javax.inject.Inject
|
|||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
private const val RETRY_WAIT_TIME_MS = 10_000L
|
private const val RETRY_WAIT_TIME_MS = 10_000L
|
||||||
private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("SyncThread", LoggerTag.SYNC)
|
private val loggerTag = LoggerTag("SyncThread", LoggerTag.SYNC)
|
||||||
|
|
||||||
@ -61,7 +61,8 @@ internal class SyncThread @Inject constructor(
|
|||||||
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
||||||
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||||
private val activeCallHandler: ActiveCallHandler,
|
private val activeCallHandler: ActiveCallHandler,
|
||||||
private val lightweightSettingsStorage: DefaultLightweightSettingsStorage
|
private val lightweightSettingsStorage: DefaultLightweightSettingsStorage,
|
||||||
|
private val matrixConfiguration: MatrixConfiguration,
|
||||||
) : Thread("Matrix-SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
) : Thread("Matrix-SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||||
|
|
||||||
private var state: SyncState = SyncState.Idle
|
private var state: SyncState = SyncState.Idle
|
||||||
@ -181,7 +182,7 @@ internal class SyncThread @Inject constructor(
|
|||||||
val timeout = when {
|
val timeout = when {
|
||||||
previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */
|
previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */
|
||||||
afterPause -> 0L /* No timeout after a pause */
|
afterPause -> 0L /* No timeout after a pause */
|
||||||
else -> DEFAULT_LONG_POOL_TIMEOUT
|
else -> matrixConfiguration.syncConfig.longPollTimeout
|
||||||
}
|
}
|
||||||
Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout")
|
Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout")
|
||||||
val presence = lightweightSettingsStorage.getSyncPresenceStatus()
|
val presence = lightweightSettingsStorage.getSyncPresenceStatus()
|
||||||
|
@ -118,3 +118,10 @@ if (hasPngs) {
|
|||||||
if (github.requested_reviewers.users.length == 0 && !pr.draft) {
|
if (github.requested_reviewers.users.length == 0 && !pr.draft) {
|
||||||
warn("Please add a reviewer to your PR.")
|
warn("Please add a reviewer to your PR.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that translations have not been modified by developers
|
||||||
|
if (user != "RiotTranslateBot") {
|
||||||
|
if (editedFiles.some(file => file.endsWith("strings.xml") && !file.endsWith("values/strings.xml"))) {
|
||||||
|
fail("Some translation files have been edited. Only user `RiotTranslateBot` (i.e. translations coming from Weblate) is allowed to do that.\nPlease read more about translations management [in the doc](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#internationalisation).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,6 +19,9 @@
|
|||||||
<issue id="IconExpectedSize" severity="error" />
|
<issue id="IconExpectedSize" severity="error" />
|
||||||
<issue id="LocaleFolder" severity="error" />
|
<issue id="LocaleFolder" severity="error" />
|
||||||
|
|
||||||
|
<!-- AlwaysShowAction is considered as an error to force ignoring the issue when detected -->
|
||||||
|
<issue id="AlwaysShowAction" severity="error" />
|
||||||
|
|
||||||
<issue id="TooManyViews" severity="warning">
|
<issue id="TooManyViews" severity="warning">
|
||||||
<!-- Ignore TooManyViews in debug build type -->
|
<!-- Ignore TooManyViews in debug build type -->
|
||||||
<ignore path="**/src/debug/**" />
|
<ignore path="**/src/debug/**" />
|
||||||
@ -77,6 +80,7 @@
|
|||||||
<issue id="KotlinPropertyAccess" severity="error" />
|
<issue id="KotlinPropertyAccess" severity="error" />
|
||||||
<issue id="DefaultLocale" severity="error" />
|
<issue id="DefaultLocale" severity="error" />
|
||||||
<issue id="CheckResult" severity="error" />
|
<issue id="CheckResult" severity="error" />
|
||||||
|
<issue id="StaticFieldLeak" severity="error" />
|
||||||
|
|
||||||
<issue id="InvalidPackage">
|
<issue id="InvalidPackage">
|
||||||
<!-- Ignore error from HtmlCompressor lib -->
|
<!-- Ignore error from HtmlCompressor lib -->
|
||||||
@ -87,6 +91,7 @@
|
|||||||
|
|
||||||
<!-- Manifest -->
|
<!-- Manifest -->
|
||||||
<issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" />
|
<issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" />
|
||||||
|
<issue id="DataExtractionRules" severity="error" />
|
||||||
|
|
||||||
<!-- Performance -->
|
<!-- Performance -->
|
||||||
<issue id="UselessParent" severity="error" />
|
<issue id="UselessParent" severity="error" />
|
||||||
@ -104,6 +109,9 @@
|
|||||||
<issue id="TypographyDashes" severity="error" />
|
<issue id="TypographyDashes" severity="error" />
|
||||||
<issue id="PluralsCandidate" severity="error" />
|
<issue id="PluralsCandidate" severity="error" />
|
||||||
|
|
||||||
|
<!-- Notification -->
|
||||||
|
<issue id="LaunchActivityFromNotification" severity="error" />
|
||||||
|
|
||||||
<!-- DI -->
|
<!-- DI -->
|
||||||
<issue id="JvmStaticProvidesInObjectDetector" severity="error" />
|
<issue id="JvmStaticProvidesInObjectDetector" severity="error" />
|
||||||
</lint>
|
</lint>
|
||||||
|
@ -191,7 +191,7 @@ android {
|
|||||||
// Known limitation: it does not modify the value in the BuildConfig.java generated file
|
// Known limitation: it does not modify the value in the BuildConfig.java generated file
|
||||||
// See https://issuetracker.google.com/issues/171133218
|
// See https://issuetracker.google.com/issues/171133218
|
||||||
output.versionCodeOverride = baseVariantVersion + baseAbiVersionCode
|
output.versionCodeOverride = baseVariantVersion + baseAbiVersionCode
|
||||||
print "ABI " + output.getFilter(OutputFile.ABI) + " \t-> VersionCode = " + output.versionCodeOverride + "\n"
|
print "ABI " + output.getFilter(OutputFile.ABI) + " \t-> VersionCode = " + output.versionCode + "\n"
|
||||||
output.outputFileName = output.outputFileName.replace("vector-app", "vector")
|
output.outputFileName = output.outputFileName.replace("vector-app", "vector")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import androidx.test.filters.LargeTest
|
|||||||
import com.adevinta.android.barista.internal.viewaction.SleepViewAction
|
import com.adevinta.android.barista.internal.viewaction.SleepViewAction
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.ui.robot.ElementRobot
|
import im.vector.app.ui.robot.ElementRobot
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.RuleChain
|
import org.junit.rules.RuleChain
|
||||||
@ -34,6 +35,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
|
@Ignore("Disabled temporarily so that we can unblock other PRs.")
|
||||||
class CantVerifyTest {
|
class CantVerifyTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
@ -225,8 +225,8 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
|
|||||||
|
|
||||||
// Wait until local secrets are known (gossip)
|
// Wait until local secrets are known (gossip)
|
||||||
withIdlingResource(allSecretsKnownIdling(uiSession)) {
|
withIdlingResource(allSecretsKnownIdling(uiSession)) {
|
||||||
onView(withId(R.id.roomListContainer))
|
onView(withId(R.id.groupToolbarAvatarImageView))
|
||||||
.check(matches(isDisplayed()))
|
.perform(click())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,11 +20,13 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||||||
import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
|
import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
|
||||||
import org.matrix.android.sdk.api.Matrix
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
|
import org.matrix.android.sdk.api.SyncConfig
|
||||||
|
|
||||||
fun getMatrixInstance(): Matrix {
|
fun getMatrixInstance(): Matrix {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val configuration = MatrixConfiguration(
|
val configuration = MatrixConfiguration(
|
||||||
roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context)
|
roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context),
|
||||||
|
syncConfig = SyncConfig(longPollTimeout = 5_000L),
|
||||||
)
|
)
|
||||||
return Matrix(context, configuration)
|
return Matrix(context, configuration)
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,6 @@ private fun useMediaStoreScreenshotStorage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun usePublicExternalScreenshotStorage(
|
private fun usePublicExternalScreenshotStorage(
|
||||||
contentValues: ContentValues,
|
contentValues: ContentValues,
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
|
@ -50,7 +50,7 @@ import im.vector.app.withIdlingResource
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ElementRobot(
|
class ElementRobot(
|
||||||
private val labsPreferences: LabFeaturesPreferences = LabFeaturesPreferences(true)
|
private val labsPreferences: LabFeaturesPreferences = LabFeaturesPreferences(false)
|
||||||
) {
|
) {
|
||||||
fun onboarding(block: OnboardingRobot.() -> Unit) {
|
fun onboarding(block: OnboardingRobot.() -> Unit) {
|
||||||
block(OnboardingRobot())
|
block(OnboardingRobot())
|
||||||
|
@ -23,7 +23,7 @@ import android.content.IntentFilter
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import im.vector.app.core.debug.DebugReceiver
|
import im.vector.app.core.debug.DebugReceiver
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
import im.vector.app.core.utils.lsFiles
|
import im.vector.app.core.utils.lsFiles
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -31,7 +31,10 @@ import javax.inject.Inject
|
|||||||
/**
|
/**
|
||||||
* Receiver to handle some command from ADB
|
* Receiver to handle some command from ADB
|
||||||
*/
|
*/
|
||||||
class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugReceiver {
|
class VectorDebugReceiver @Inject constructor(
|
||||||
|
@DefaultPreferences
|
||||||
|
private val sharedPreferences: SharedPreferences,
|
||||||
|
) : BroadcastReceiver(), DebugReceiver {
|
||||||
|
|
||||||
override fun register(context: Context) {
|
override fun register(context: Context) {
|
||||||
context.registerReceiver(this, getIntentFilter(context))
|
context.registerReceiver(this, getIntentFilter(context))
|
||||||
@ -47,14 +50,14 @@ class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugRece
|
|||||||
intent.action?.let {
|
intent.action?.let {
|
||||||
when {
|
when {
|
||||||
it.endsWith(DEBUG_ACTION_DUMP_FILESYSTEM) -> lsFiles(context)
|
it.endsWith(DEBUG_ACTION_DUMP_FILESYSTEM) -> lsFiles(context)
|
||||||
it.endsWith(DEBUG_ACTION_DUMP_PREFERENCES) -> dumpPreferences(context)
|
it.endsWith(DEBUG_ACTION_DUMP_PREFERENCES) -> dumpPreferences()
|
||||||
it.endsWith(DEBUG_ACTION_ALTER_SCALAR_TOKEN) -> alterScalarToken(context)
|
it.endsWith(DEBUG_ACTION_ALTER_SCALAR_TOKEN) -> alterScalarToken()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dumpPreferences(context: Context) {
|
private fun dumpPreferences() {
|
||||||
logPrefs("DefaultSharedPreferences", DefaultSharedPreferences.getInstance(context))
|
logPrefs("DefaultSharedPreferences", sharedPreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logPrefs(name: String, sharedPreferences: SharedPreferences?) {
|
private fun logPrefs(name: String, sharedPreferences: SharedPreferences?) {
|
||||||
@ -67,8 +70,8 @@ class VectorDebugReceiver @Inject constructor() : BroadcastReceiver(), DebugRece
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun alterScalarToken(context: Context) {
|
private fun alterScalarToken() {
|
||||||
DefaultSharedPreferences.getInstance(context).edit {
|
sharedPreferences.edit {
|
||||||
// putString("SCALAR_TOKEN_PREFERENCE_KEY" + Matrix.getInstance(context).defaultSession.myUserId, "bad_token")
|
// putString("SCALAR_TOKEN_PREFERENCE_KEY" + Matrix.getInstance(context).defaultSession.myUserId, "bad_token")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ package im.vector.app.push.fcm
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.google.android.gms.common.ConnectionResult
|
import com.google.android.gms.common.ConnectionResult
|
||||||
@ -24,7 +25,7 @@ import com.google.android.gms.common.GoogleApiAvailability
|
|||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
import im.vector.app.core.pushers.FcmHelper
|
import im.vector.app.core.pushers.FcmHelper
|
||||||
import im.vector.app.core.pushers.PushersManager
|
import im.vector.app.core.pushers.PushersManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -35,14 +36,13 @@ import javax.inject.Inject
|
|||||||
* It has an alter ego in the fdroid variant.
|
* It has an alter ego in the fdroid variant.
|
||||||
*/
|
*/
|
||||||
class GoogleFcmHelper @Inject constructor(
|
class GoogleFcmHelper @Inject constructor(
|
||||||
context: Context,
|
@DefaultPreferences
|
||||||
|
private val sharedPrefs: SharedPreferences,
|
||||||
) : FcmHelper {
|
) : FcmHelper {
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
|
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sharedPrefs = DefaultSharedPreferences.getInstance(context)
|
|
||||||
|
|
||||||
override fun isFirebaseAvailable(): Boolean = true
|
override fun isFirebaseAvailable(): Boolean = true
|
||||||
|
|
||||||
override fun getFcmToken(): String? {
|
override fun getFcmToken(): String? {
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
<application
|
<application
|
||||||
android:name="im.vector.app.VectorApplication"
|
android:name="im.vector.app.VectorApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
@ -53,7 +53,7 @@ import im.vector.app.core.resources.BuildMeta
|
|||||||
import im.vector.app.features.analytics.VectorAnalytics
|
import im.vector.app.features.analytics.VectorAnalytics
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.configuration.VectorConfiguration
|
import im.vector.app.features.configuration.VectorConfiguration
|
||||||
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
|
import im.vector.app.features.disclaimer.DisclaimerDialog
|
||||||
import im.vector.app.features.invite.InvitesAcceptor
|
import im.vector.app.features.invite.InvitesAcceptor
|
||||||
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
|
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
@ -109,6 +109,8 @@ class VectorApplication :
|
|||||||
@Inject lateinit var fcmHelper: FcmHelper
|
@Inject lateinit var fcmHelper: FcmHelper
|
||||||
@Inject lateinit var buildMeta: BuildMeta
|
@Inject lateinit var buildMeta: BuildMeta
|
||||||
@Inject lateinit var leakDetector: LeakDetector
|
@Inject lateinit var leakDetector: LeakDetector
|
||||||
|
@Inject lateinit var vectorLocale: VectorLocale
|
||||||
|
@Inject lateinit var disclaimerDialog: DisclaimerDialog
|
||||||
|
|
||||||
// font thread handler
|
// font thread handler
|
||||||
private var fontThreadHandler: Handler? = null
|
private var fontThreadHandler: Handler? = null
|
||||||
@ -159,7 +161,7 @@ class VectorApplication :
|
|||||||
R.array.com_google_android_gms_fonts_certs
|
R.array.com_google_android_gms_fonts_certs
|
||||||
)
|
)
|
||||||
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
|
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
|
||||||
VectorLocale.init(this, buildMeta)
|
vectorLocale.init()
|
||||||
ThemeUtils.init(this)
|
ThemeUtils.init(this)
|
||||||
vectorConfiguration.applyToApplicationContext()
|
vectorConfiguration.applyToApplicationContext()
|
||||||
|
|
||||||
@ -171,7 +173,7 @@ class VectorApplication :
|
|||||||
val sessionImported = legacySessionImporter.process()
|
val sessionImported = legacySessionImporter.process()
|
||||||
if (!sessionImported) {
|
if (!sessionImported) {
|
||||||
// Do not display the name change popup
|
// Do not display the name change popup
|
||||||
doNotShowDisclaimerDialog(this)
|
disclaimerDialog.doNotShowDisclaimerDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
|
18
vector-app/src/main/res/xml/backup_rules.xml
Normal file
18
vector-app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<full-backup-content>
|
||||||
|
<exclude
|
||||||
|
domain="file"
|
||||||
|
path="." />
|
||||||
|
<exclude
|
||||||
|
domain="database"
|
||||||
|
path="." />
|
||||||
|
<exclude
|
||||||
|
domain="sharedpref"
|
||||||
|
path="." />
|
||||||
|
<exclude
|
||||||
|
domain="external"
|
||||||
|
path="." />
|
||||||
|
<exclude
|
||||||
|
domain="root"
|
||||||
|
path="." />
|
||||||
|
</full-backup-content>
|
17
vector-app/src/main/res/xml/data_extraction_rules.xml
Normal file
17
vector-app/src/main/res/xml/data_extraction_rules.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<exclude domain="root" />
|
||||||
|
<exclude domain="file" />
|
||||||
|
<exclude domain="database" />
|
||||||
|
<exclude domain="sharedpref" />
|
||||||
|
<exclude domain="external" />
|
||||||
|
</cloud-backup>
|
||||||
|
<device-transfer>
|
||||||
|
<exclude domain="root" />
|
||||||
|
<exclude domain="file" />
|
||||||
|
<exclude domain="database" />
|
||||||
|
<exclude domain="sharedpref" />
|
||||||
|
<exclude domain="external" />
|
||||||
|
</device-transfer>
|
||||||
|
</data-extraction-rules>
|
@ -40,7 +40,7 @@
|
|||||||
<bool name="settings_labs_deferred_dm_visible">true</bool>
|
<bool name="settings_labs_deferred_dm_visible">true</bool>
|
||||||
<bool name="settings_labs_deferred_dm_default">true</bool>
|
<bool name="settings_labs_deferred_dm_default">true</bool>
|
||||||
<bool name="settings_labs_thread_messages_default">false</bool>
|
<bool name="settings_labs_thread_messages_default">false</bool>
|
||||||
<bool name="settings_labs_new_app_layout_default">true</bool>
|
<bool name="settings_labs_new_app_layout_default">false</bool>
|
||||||
<bool name="settings_timeline_show_live_sender_info_visible">true</bool>
|
<bool name="settings_timeline_show_live_sender_info_visible">true</bool>
|
||||||
<bool name="settings_timeline_show_live_sender_info_default">false</bool>
|
<bool name="settings_timeline_show_live_sender_info_default">false</bool>
|
||||||
<!-- Level 1: Advanced settings -->
|
<!-- Level 1: Advanced settings -->
|
||||||
|
@ -230,9 +230,9 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnifiedPush
|
// UnifiedPush
|
||||||
implementation 'com.github.UnifiedPush:android-connector:2.0.1'
|
implementation 'com.github.UnifiedPush:android-connector:2.1.0'
|
||||||
|
|
||||||
implementation "androidx.emoji2:emoji2:1.1.0"
|
implementation "androidx.emoji2:emoji2:1.2.0"
|
||||||
|
|
||||||
// WebRTC
|
// WebRTC
|
||||||
// org.webrtc:google-webrtc is for development purposes only
|
// org.webrtc:google-webrtc is for development purposes only
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.app
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import java.lang.reflect.Field
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to override [Build.VERSION.SDK_INT]. Ideally an interface should be used instead, but that approach forces us to either add suppress lint annotations
|
|
||||||
* and potentially miss an API version issue or write a custom lint rule, which seems like an overkill.
|
|
||||||
*/
|
|
||||||
object AndroidVersionTestOverrider {
|
|
||||||
|
|
||||||
private var initialValue: Int? = null
|
|
||||||
|
|
||||||
fun override(newVersion: Int) {
|
|
||||||
if (initialValue == null) {
|
|
||||||
initialValue = Build.VERSION.SDK_INT
|
|
||||||
}
|
|
||||||
val field = Build.VERSION::class.java.getField("SDK_INT")
|
|
||||||
setStaticField(field, newVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restore() {
|
|
||||||
initialValue?.let { override(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setStaticField(field: Field, value: Any) {
|
|
||||||
field.isAccessible = true
|
|
||||||
field.set(null, value)
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,8 +18,6 @@ package im.vector.app
|
|||||||
|
|
||||||
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
|
||||||
class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
|
class TestBuildVersionSdkIntProvider(var value: Int = 0) : BuildVersionSdkIntProvider {
|
||||||
var value: Int = 0
|
|
||||||
|
|
||||||
override fun get() = value
|
override fun get() = value
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import android.security.keystore.KeyProperties
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import im.vector.app.TestBuildVersionSdkIntProvider
|
||||||
import im.vector.app.features.pin.PinCodeStore
|
import im.vector.app.features.pin.PinCodeStore
|
||||||
import im.vector.app.features.pin.SharedPrefPinCodeStore
|
import im.vector.app.features.pin.SharedPrefPinCodeStore
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.ANDROID_KEY_STORE
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.ANDROID_KEY_STORE
|
||||||
@ -32,7 +33,6 @@ import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LE
|
|||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -42,7 +42,6 @@ import org.amshove.kluent.shouldBeEqualTo
|
|||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.KeyFactory
|
import java.security.KeyFactory
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
@ -66,9 +65,7 @@ class LegacyPinCodeMigratorTests {
|
|||||||
SharedPrefPinCodeStore(PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().context))
|
SharedPrefPinCodeStore(PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().context))
|
||||||
)
|
)
|
||||||
private val keyStore: KeyStore = spyk(KeyStore.getInstance(ANDROID_KEY_STORE)).also { it.load(null) }
|
private val keyStore: KeyStore = spyk(KeyStore.getInstance(ANDROID_KEY_STORE)).also { it.load(null) }
|
||||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider = mockk {
|
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider(Build.VERSION_CODES.M)
|
||||||
every { get() } returns Build.VERSION_CODES.M
|
|
||||||
}
|
|
||||||
private val secretStoringUtils: SecretStoringUtils = spyk(
|
private val secretStoringUtils: SecretStoringUtils = spyk(
|
||||||
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
|
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
|
||||||
)
|
)
|
||||||
@ -125,26 +122,18 @@ class LegacyPinCodeMigratorTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun migratePinCodeM() = runTest {
|
fun migratePinCodeM() = runTest {
|
||||||
val pinCode = "1234"
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
||||||
saveLegacyPinCode(pinCode)
|
migratePinCode()
|
||||||
|
|
||||||
legacyPinCodeMigrator.migrate()
|
|
||||||
|
|
||||||
coVerify { legacyPinCodeMigrator.getDecryptedPinCode() }
|
|
||||||
verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
|
|
||||||
coVerify { pinCodeStore.savePinCode(any()) }
|
|
||||||
verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
|
|
||||||
|
|
||||||
val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias))
|
|
||||||
decodedPinCode shouldBeEqualTo pinCode
|
|
||||||
keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false
|
|
||||||
keyStore.containsAlias(alias) shouldBe true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun migratePinCodeL() = runTest {
|
fun migratePinCodeL() = runTest {
|
||||||
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
|
||||||
|
migratePinCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun migratePinCode() {
|
||||||
val pinCode = "1234"
|
val pinCode = "1234"
|
||||||
every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP
|
|
||||||
saveLegacyPinCode(pinCode)
|
saveLegacyPinCode(pinCode)
|
||||||
|
|
||||||
legacyPinCodeMigrator.migrate()
|
legacyPinCodeMigrator.migrate()
|
||||||
@ -163,7 +152,7 @@ class LegacyPinCodeMigratorTests {
|
|||||||
private fun generateLegacyKey() {
|
private fun generateLegacyKey() {
|
||||||
if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return
|
if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return
|
||||||
|
|
||||||
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
|
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) {
|
||||||
generateLegacyKeyM()
|
generateLegacyKeyM()
|
||||||
} else {
|
} else {
|
||||||
generateLegacyKeyL()
|
generateLegacyKeyL()
|
||||||
@ -206,7 +195,7 @@ class LegacyPinCodeMigratorTests {
|
|||||||
generateLegacyKey()
|
generateLegacyKey()
|
||||||
val publicKey = keyStore.getCertificate(LEGACY_PIN_CODE_KEY_ALIAS).publicKey
|
val publicKey = keyStore.getCertificate(LEGACY_PIN_CODE_KEY_ALIAS).publicKey
|
||||||
val cipher = getLegacyCipher()
|
val cipher = getLegacyCipher()
|
||||||
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
|
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) {
|
||||||
val unrestrictedKey = KeyFactory.getInstance(publicKey.algorithm).generatePublic(X509EncodedKeySpec(publicKey.encoded))
|
val unrestrictedKey = KeyFactory.getInstance(publicKey.algorithm).generatePublic(X509EncodedKeySpec(publicKey.encoded))
|
||||||
val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT)
|
val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT)
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, unrestrictedKey, spec)
|
cipher.init(Cipher.ENCRYPT_MODE, unrestrictedKey, spec)
|
||||||
@ -219,14 +208,15 @@ class LegacyPinCodeMigratorTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getLegacyCipher(): Cipher {
|
private fun getLegacyCipher(): Cipher {
|
||||||
return when (buildVersionSdkIntProvider.get()) {
|
return if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) {
|
||||||
Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1 -> getCipherL()
|
getCipherM()
|
||||||
else -> getCipherM()
|
} else {
|
||||||
|
getCipherL()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCipherL(): Cipher {
|
private fun getCipherL(): Cipher {
|
||||||
val provider = if (buildVersionSdkIntProvider.get() < Build.VERSION_CODES.M) "AndroidOpenSSL" else "AndroidKeyStoreBCWorkaround"
|
val provider = if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) "AndroidKeyStoreBCWorkaround" else "AndroidOpenSSL"
|
||||||
val transformation = "RSA/ECB/PKCS1Padding"
|
val transformation = "RSA/ECB/PKCS1Padding"
|
||||||
return Cipher.getInstance(transformation, provider)
|
return Cipher.getInstance(transformation, provider)
|
||||||
}
|
}
|
||||||
|
@ -26,10 +26,12 @@ import org.amshove.kluent.shouldBeNull
|
|||||||
import org.amshove.kluent.shouldExist
|
import org.amshove.kluent.shouldExist
|
||||||
import org.amshove.kluent.shouldNotBeNull
|
import org.amshove.kluent.shouldNotBeNull
|
||||||
import org.amshove.kluent.shouldNotExist
|
import org.amshove.kluent.shouldNotExist
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@Ignore("Disabled temporarily so that we can unblock other PRs.")
|
||||||
class VoiceRecorderLTests {
|
class VoiceRecorderLTests {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
@ -18,41 +18,36 @@ package im.vector.app.features.voice
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import im.vector.app.AndroidVersionTestOverrider
|
import im.vector.app.TestBuildVersionSdkIntProvider
|
||||||
import im.vector.app.features.DefaultVectorFeatures
|
import im.vector.app.features.DefaultVectorFeatures
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
import org.amshove.kluent.shouldBeInstanceOf
|
import org.amshove.kluent.shouldBeInstanceOf
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class VoiceRecorderProviderTests {
|
class VoiceRecorderProviderTests {
|
||||||
|
|
||||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
private val provider = spyk(VoiceRecorderProvider(context, DefaultVectorFeatures()))
|
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
|
||||||
|
private val provider = spyk(VoiceRecorderProvider(context, DefaultVectorFeatures(), buildVersionSdkIntProvider))
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
AndroidVersionTestOverrider.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun provideVoiceRecorderOnAndroidQAndCodecReturnsQRecorder() {
|
fun provideVoiceRecorderOnAndroidQAndCodecReturnsQRecorder() {
|
||||||
AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q)
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.Q
|
||||||
every { provider.hasOpusEncoder() } returns true
|
every { provider.hasOpusEncoder() } returns true
|
||||||
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderQ::class)
|
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderQ::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun provideVoiceRecorderOnAndroidQButNoCodecReturnsLRecorder() {
|
fun provideVoiceRecorderOnAndroidQButNoCodecReturnsLRecorder() {
|
||||||
AndroidVersionTestOverrider.override(Build.VERSION_CODES.Q)
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.Q
|
||||||
every { provider.hasOpusEncoder() } returns false
|
every { provider.hasOpusEncoder() } returns false
|
||||||
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class)
|
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun provideVoiceRecorderOnOlderAndroidVersionReturnsLRecorder() {
|
fun provideVoiceRecorderOnOlderAndroidVersionReturnsLRecorder() {
|
||||||
AndroidVersionTestOverrider.override(Build.VERSION_CODES.LOLLIPOP)
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
|
||||||
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class)
|
provider.provideVoiceRecorder().shouldBeInstanceOf(VoiceRecorderL::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.core.extensions
|
package im.vector.app.core.extensions
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
@ -91,11 +90,9 @@ fun Context.safeOpenOutputStream(uri: Uri): OutputStream? {
|
|||||||
*
|
*
|
||||||
* @return true if no active connection is found
|
* @return true if no active connection is found
|
||||||
*/
|
*/
|
||||||
@Suppress("deprecation")
|
|
||||||
@SuppressLint("NewApi") // false positive
|
|
||||||
fun Context.inferNoConnectivity(sdkIntProvider: BuildVersionSdkIntProvider): Boolean {
|
fun Context.inferNoConnectivity(sdkIntProvider: BuildVersionSdkIntProvider): Boolean {
|
||||||
val connectivityManager = getSystemService<ConnectivityManager>()!!
|
val connectivityManager = getSystemService<ConnectivityManager>()!!
|
||||||
return if (sdkIntProvider.get() > Build.VERSION_CODES.M) {
|
return if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.M)) {
|
||||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||||
when {
|
when {
|
||||||
networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false
|
networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false
|
||||||
@ -104,6 +101,7 @@ fun Context.inferNoConnectivity(sdkIntProvider: BuildVersionSdkIntProvider): Boo
|
|||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
when (connectivityManager.activeNetworkInfo?.type) {
|
when (connectivityManager.activeNetworkInfo?.type) {
|
||||||
ConnectivityManager.TYPE_WIFI -> false
|
ConnectivityManager.TYPE_WIFI -> false
|
||||||
ConnectivityManager.TYPE_MOBILE -> false
|
ConnectivityManager.TYPE_MOBILE -> false
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.core.platform
|
package im.vector.app.core.platform
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@ -85,6 +84,7 @@ import im.vector.app.features.rageshake.RageShake
|
|||||||
import im.vector.app.features.session.SessionListener
|
import im.vector.app.features.session.SessionListener
|
||||||
import im.vector.app.features.settings.FontScalePreferences
|
import im.vector.app.features.settings.FontScalePreferences
|
||||||
import im.vector.app.features.settings.FontScalePreferencesImpl
|
import im.vector.app.features.settings.FontScalePreferencesImpl
|
||||||
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.themes.ActivityOtherThemes
|
import im.vector.app.features.themes.ActivityOtherThemes
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
@ -156,6 +156,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
|||||||
@Inject lateinit var rageShake: RageShake
|
@Inject lateinit var rageShake: RageShake
|
||||||
@Inject lateinit var buildMeta: BuildMeta
|
@Inject lateinit var buildMeta: BuildMeta
|
||||||
@Inject lateinit var fontScalePreferences: FontScalePreferences
|
@Inject lateinit var fontScalePreferences: FontScalePreferences
|
||||||
|
@Inject lateinit var vectorLocale: VectorLocaleProvider
|
||||||
|
|
||||||
// For debug only
|
// For debug only
|
||||||
@Inject lateinit var debugReceiver: DebugReceiver
|
@Inject lateinit var debugReceiver: DebugReceiver
|
||||||
@ -177,8 +178,10 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
|||||||
private val restorables = ArrayList<Restorable>()
|
private val restorables = ArrayList<Restorable>()
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
val fontScalePreferences = FontScalePreferencesImpl(PreferenceManager.getDefaultSharedPreferences(base), AndroidSystemSettingsProvider(base))
|
val preferences = PreferenceManager.getDefaultSharedPreferences(base)
|
||||||
val vectorConfiguration = VectorConfiguration(this, fontScalePreferences)
|
val fontScalePreferences = FontScalePreferencesImpl(preferences, AndroidSystemSettingsProvider(base))
|
||||||
|
val vectorLocaleProvider = VectorLocaleProvider(preferences)
|
||||||
|
val vectorConfiguration = VectorConfiguration(this, fontScalePreferences, vectorLocaleProvider)
|
||||||
super.attachBaseContext(vectorConfiguration.getLocalisedContext(base))
|
super.attachBaseContext(vectorConfiguration.getLocalisedContext(base))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,23 +467,18 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
|||||||
/**
|
/**
|
||||||
* Force to render the activity in fullscreen.
|
* Force to render the activity in fullscreen.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun setFullScreen() {
|
private fun setFullScreen() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
window.setDecorFitsSystemWindows(false)
|
window.setDecorFitsSystemWindows(false)
|
||||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
} else {
|
|
||||||
@SuppressLint("WrongConstant")
|
|
||||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
|
||||||
}
|
|
||||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||||
window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
|
window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
|
||||||
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
|
window.navigationBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
|
||||||
} else {
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
@ -17,16 +17,17 @@
|
|||||||
package im.vector.app.core.pushers
|
package im.vector.app.core.pushers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class UnifiedPushStore @Inject constructor(
|
class UnifiedPushStore @Inject constructor(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val fcmHelper: FcmHelper
|
val fcmHelper: FcmHelper,
|
||||||
|
@DefaultPreferences
|
||||||
|
private val defaultPrefs: SharedPreferences,
|
||||||
) {
|
) {
|
||||||
private val defaultPrefs = DefaultSharedPreferences.getInstance(context)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the UnifiedPush Endpoint.
|
* Retrieves the UnifiedPush Endpoint.
|
||||||
*
|
*
|
||||||
|
@ -20,11 +20,10 @@ import android.content.Context
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
|
||||||
import im.vector.app.databinding.ViewKeysBackupBannerBinding
|
import im.vector.app.databinding.ViewKeysBackupBannerBinding
|
||||||
|
import im.vector.app.features.workers.signout.BannerState
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,16 +37,12 @@ class KeysBackupBanner @JvmOverloads constructor(
|
|||||||
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
|
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
|
||||||
|
|
||||||
var delegate: Delegate? = null
|
var delegate: Delegate? = null
|
||||||
private var state: State = State.Initial
|
private var state: BannerState = BannerState.Initial
|
||||||
|
|
||||||
private lateinit var views: ViewKeysBackupBannerBinding
|
private lateinit var views: ViewKeysBackupBannerBinding
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupView()
|
setupView()
|
||||||
DefaultSharedPreferences.getInstance(context).edit {
|
|
||||||
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
|
|
||||||
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,7 +51,7 @@ class KeysBackupBanner @JvmOverloads constructor(
|
|||||||
* @param newState the newState representing the view
|
* @param newState the newState representing the view
|
||||||
* @param force true to force the rendering of the view
|
* @param force true to force the rendering of the view
|
||||||
*/
|
*/
|
||||||
fun render(newState: State, force: Boolean = false) {
|
fun render(newState: BannerState, force: Boolean = false) {
|
||||||
if (newState == state && !force) {
|
if (newState == state && !force) {
|
||||||
Timber.v("State unchanged")
|
Timber.v("State unchanged")
|
||||||
return
|
return
|
||||||
@ -67,48 +62,26 @@ class KeysBackupBanner @JvmOverloads constructor(
|
|||||||
|
|
||||||
hideAll()
|
hideAll()
|
||||||
when (newState) {
|
when (newState) {
|
||||||
State.Initial -> renderInitial()
|
BannerState.Initial -> renderInitial()
|
||||||
State.Hidden -> renderHidden()
|
BannerState.Hidden -> renderHidden()
|
||||||
is State.Setup -> renderSetup(newState.numberOfKeys)
|
is BannerState.Setup -> renderSetup(newState)
|
||||||
is State.Recover -> renderRecover(newState.version)
|
is BannerState.Recover -> renderRecover(newState)
|
||||||
is State.Update -> renderUpdate(newState.version)
|
is BannerState.Update -> renderUpdate(newState)
|
||||||
State.BackingUp -> renderBackingUp()
|
BannerState.BackingUp -> renderBackingUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is State.Setup -> delegate?.setupKeysBackup()
|
is BannerState.Setup -> delegate?.setupKeysBackup()
|
||||||
is State.Update,
|
is BannerState.Update,
|
||||||
is State.Recover -> delegate?.recoverKeysBackup()
|
is BannerState.Recover -> delegate?.recoverKeysBackup()
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCloseClicked() {
|
private fun onCloseClicked() {
|
||||||
state.let {
|
delegate?.onCloseClicked()
|
||||||
when (it) {
|
|
||||||
is State.Setup -> {
|
|
||||||
DefaultSharedPreferences.getInstance(context).edit {
|
|
||||||
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is State.Recover -> {
|
|
||||||
DefaultSharedPreferences.getInstance(context).edit {
|
|
||||||
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is State.Update -> {
|
|
||||||
DefaultSharedPreferences.getInstance(context).edit {
|
|
||||||
putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, it.version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Should not happen, close button is not displayed in other cases
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force refresh
|
// Force refresh
|
||||||
render(state, true)
|
render(state, true)
|
||||||
}
|
}
|
||||||
@ -133,9 +106,8 @@ class KeysBackupBanner @JvmOverloads constructor(
|
|||||||
isVisible = false
|
isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderSetup(nbOfKeys: Int) {
|
private fun renderSetup(state: BannerState.Setup) {
|
||||||
if (nbOfKeys == 0 ||
|
if (state.numberOfKeys == 0 || state.doNotShowAgain) {
|
||||||
DefaultSharedPreferences.getInstance(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) {
|
|
||||||
// Do not display the setup banner if there is no keys to backup, or if the user has already closed it
|
// Do not display the setup banner if there is no keys to backup, or if the user has already closed it
|
||||||
isVisible = false
|
isVisible = false
|
||||||
} else {
|
} else {
|
||||||
@ -148,8 +120,8 @@ class KeysBackupBanner @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderRecover(version: String) {
|
private fun renderRecover(state: BannerState.Recover) {
|
||||||
if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) {
|
if (state.version == state.doNotShowForVersion) {
|
||||||
isVisible = false
|
isVisible = false
|
||||||
} else {
|
} else {
|
||||||
isVisible = true
|
isVisible = true
|
||||||
@ -161,8 +133,8 @@ class KeysBackupBanner @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderUpdate(version: String) {
|
private fun renderUpdate(state: BannerState.Update) {
|
||||||
if (version == DefaultSharedPreferences.getInstance(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) {
|
if (state.version == state.doNotShowForVersion) {
|
||||||
isVisible = false
|
isVisible = false
|
||||||
} else {
|
} else {
|
||||||
isVisible = true
|
isVisible = true
|
||||||
@ -191,61 +163,12 @@ class KeysBackupBanner @JvmOverloads constructor(
|
|||||||
views.viewKeysBackupBannerLoading.isVisible = false
|
views.viewKeysBackupBannerLoading.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The state representing the view.
|
|
||||||
* It can take one state at a time.
|
|
||||||
*/
|
|
||||||
sealed class State {
|
|
||||||
// Not yet rendered
|
|
||||||
object Initial : State()
|
|
||||||
|
|
||||||
// View will be Gone
|
|
||||||
object Hidden : State()
|
|
||||||
|
|
||||||
// Keys backup is not setup, numberOfKeys is the number of locally stored keys
|
|
||||||
data class Setup(val numberOfKeys: Int) : State()
|
|
||||||
|
|
||||||
// Keys backup can be recovered, with version from the server
|
|
||||||
data class Recover(val version: String) : State()
|
|
||||||
|
|
||||||
// Keys backup can be updated
|
|
||||||
data class Update(val version: String) : State()
|
|
||||||
|
|
||||||
// Keys are backing up
|
|
||||||
object BackingUp : State()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface to delegate some actions to another object.
|
* An interface to delegate some actions to another object.
|
||||||
*/
|
*/
|
||||||
interface Delegate {
|
interface Delegate {
|
||||||
|
fun onCloseClicked()
|
||||||
fun setupKeysBackup()
|
fun setupKeysBackup()
|
||||||
fun recoverKeysBackup()
|
fun recoverKeysBackup()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Preference key for setup. Value is a boolean.
|
|
||||||
*/
|
|
||||||
private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preference key for recover. Value is a backup version (String).
|
|
||||||
*/
|
|
||||||
private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preference key for update. Value is a backup version (String).
|
|
||||||
*/
|
|
||||||
private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version.
|
|
||||||
*/
|
|
||||||
fun onRecoverDoneForVersion(context: Context, version: String) {
|
|
||||||
DefaultSharedPreferences.getInstance(context).edit {
|
|
||||||
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -309,7 +309,6 @@ suspend fun saveMedia(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun saveMediaLegacy(
|
private fun saveMediaLegacy(
|
||||||
context: Context,
|
context: Context,
|
||||||
mediaMimeType: String?,
|
mediaMimeType: String?,
|
||||||
@ -340,6 +339,7 @@ private fun saveMediaLegacy(
|
|||||||
val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename, currentTimeMillis)
|
val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename, currentTimeMillis)
|
||||||
if (savedFile != null) {
|
if (savedFile != null) {
|
||||||
val downloadManager = context.getSystemService<DownloadManager>()
|
val downloadManager = context.getSystemService<DownloadManager>()
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
downloadManager?.addCompletedDownload(
|
downloadManager?.addCompletedDownload(
|
||||||
savedFile.name,
|
savedFile.name,
|
||||||
title,
|
title,
|
||||||
@ -430,7 +430,6 @@ fun selectTxtFileToWrite(
|
|||||||
* @param currentTimeMillis the current time in milliseconds
|
* @param currentTimeMillis the current time in milliseconds
|
||||||
* @return the created file
|
* @return the created file
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?, currentTimeMillis: Long): File? {
|
fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?, currentTimeMillis: Long): File? {
|
||||||
// defines another name for the external media
|
// defines another name for the external media
|
||||||
var dstFileName: String
|
var dstFileName: String
|
||||||
|
@ -17,19 +17,25 @@
|
|||||||
package im.vector.app.core.utils
|
package im.vector.app.core.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.media.Ringtone
|
import android.media.Ringtone
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file manages the sound ringtone for calls.
|
* This class manages the sound ringtone for calls.
|
||||||
* It allows you to use the default Riot Ringtone, or the standard ringtone or set a different one from the available choices
|
* It allows you to use the default Element Ringtone, or the standard ringtone or set a different one from the available choices
|
||||||
* in Android.
|
* in Android.
|
||||||
*/
|
*/
|
||||||
|
class RingtoneUtils @Inject constructor(
|
||||||
|
@DefaultPreferences
|
||||||
|
private val sharedPreferences: SharedPreferences,
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Returns a Uri object that points to a specific Ringtone.
|
* Returns a Uri object that points to a specific Ringtone.
|
||||||
*
|
*
|
||||||
@ -39,8 +45,8 @@ import im.vector.app.features.settings.VectorPreferences
|
|||||||
* @return the [Uri] of the currently set [Ringtone]
|
* @return the [Uri] of the currently set [Ringtone]
|
||||||
* @see Ringtone
|
* @see Ringtone
|
||||||
*/
|
*/
|
||||||
fun getCallRingtoneUri(context: Context): Uri? {
|
fun getCallRingtoneUri(): Uri? {
|
||||||
val callRingtone: String? = DefaultSharedPreferences.getInstance(context)
|
val callRingtone: String? = sharedPreferences
|
||||||
.getString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null)
|
.getString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null)
|
||||||
|
|
||||||
callRingtone?.let {
|
callRingtone?.let {
|
||||||
@ -65,8 +71,8 @@ fun getCallRingtoneUri(context: Context): Uri? {
|
|||||||
* @return the currently set [Ringtone]
|
* @return the currently set [Ringtone]
|
||||||
* @see Ringtone
|
* @see Ringtone
|
||||||
*/
|
*/
|
||||||
fun getCallRingtone(context: Context): Ringtone? {
|
fun getCallRingtone(): Ringtone? {
|
||||||
getCallRingtoneUri(context)?.let {
|
getCallRingtoneUri()?.let {
|
||||||
// Note that it can also return null
|
// Note that it can also return null
|
||||||
return RingtoneManager.getRingtone(context, it)
|
return RingtoneManager.getRingtone(context, it)
|
||||||
}
|
}
|
||||||
@ -83,19 +89,18 @@ fun getCallRingtone(context: Context): Ringtone? {
|
|||||||
* @return the name of the currently set [Ringtone], or null
|
* @return the name of the currently set [Ringtone], or null
|
||||||
* @see Ringtone
|
* @see Ringtone
|
||||||
*/
|
*/
|
||||||
fun getCallRingtoneName(context: Context): String? {
|
fun getCallRingtoneName(): String? {
|
||||||
return getCallRingtone(context)?.getTitle(context)
|
return getCallRingtone()?.getTitle(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the selected ringtone for riot calls.
|
* Sets the selected ringtone for riot calls.
|
||||||
*
|
*
|
||||||
* @param context Android context
|
|
||||||
* @param ringtoneUri
|
* @param ringtoneUri
|
||||||
* @see Ringtone
|
* @see Ringtone
|
||||||
*/
|
*/
|
||||||
fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) {
|
fun setCallRingtoneUri(ringtoneUri: Uri) {
|
||||||
DefaultSharedPreferences.getInstance(context)
|
sharedPreferences
|
||||||
.edit {
|
.edit {
|
||||||
putString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString())
|
putString(VectorPreferences.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString())
|
||||||
}
|
}
|
||||||
@ -104,16 +109,17 @@ fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) {
|
|||||||
/**
|
/**
|
||||||
* Set using Riot default ringtone.
|
* Set using Riot default ringtone.
|
||||||
*/
|
*/
|
||||||
fun useRiotDefaultRingtone(context: Context): Boolean {
|
fun useRiotDefaultRingtone(): Boolean {
|
||||||
return DefaultSharedPreferences.getInstance(context).getBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true)
|
return sharedPreferences.getBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask if default Riot ringtone has to be used.
|
* Ask if default Riot ringtone has to be used.
|
||||||
*/
|
*/
|
||||||
fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) {
|
fun setUseRiotDefaultRingtone(useRiotDefault: Boolean) {
|
||||||
DefaultSharedPreferences.getInstance(context)
|
sharedPreferences
|
||||||
.edit {
|
.edit {
|
||||||
putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault)
|
putBoolean(VectorPreferences.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -169,11 +169,11 @@ class AttachmentsPreviewFragment :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun applyInsets() {
|
private fun applyInsets() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
activity?.window?.setDecorFitsSystemWindows(false)
|
activity?.window?.setDecorFitsSystemWindows(false)
|
||||||
} else {
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
}
|
}
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(views.attachmentPreviewerBottomContainer) { v, insets ->
|
ViewCompat.setOnApplyWindowInsetsListener(views.attachmentPreviewerBottomContainer) { v, insets ->
|
||||||
|
@ -25,7 +25,7 @@ import im.vector.app.core.utils.toBase32String
|
|||||||
import im.vector.app.features.call.conference.jwt.JitsiJWTFactory
|
import im.vector.app.features.call.conference.jwt.JitsiJWTFactory
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
import im.vector.app.features.themes.ThemeProvider
|
import im.vector.app.features.themes.ThemeProvider
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.jitsi.meet.sdk.JitsiMeetUserInfo
|
import org.jitsi.meet.sdk.JitsiMeetUserInfo
|
||||||
@ -49,6 +49,7 @@ class JitsiService @Inject constructor(
|
|||||||
private val themeProvider: ThemeProvider,
|
private val themeProvider: ThemeProvider,
|
||||||
private val jitsiJWTFactory: JitsiJWTFactory,
|
private val jitsiJWTFactory: JitsiJWTFactory,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
private val vectorLocale: VectorLocaleProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -163,7 +164,7 @@ class JitsiService @Inject constructor(
|
|||||||
if (widgetSessionId.length > 8) {
|
if (widgetSessionId.length > 8) {
|
||||||
widgetSessionId = widgetSessionId.substring(0, 7)
|
widgetSessionId = widgetSessionId.substring(0, 7)
|
||||||
}
|
}
|
||||||
roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale)
|
roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(vectorLocale.applicationLocale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,12 +20,15 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.addChildFragment
|
import im.vector.app.core.extensions.addChildFragment
|
||||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
import im.vector.app.databinding.BottomSheetCallDialPadBinding
|
import im.vector.app.databinding.BottomSheetCallDialPadBinding
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialPadBinding>() {
|
class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialPadBinding>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -41,6 +44,8 @@ class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject lateinit var vectorLocale: VectorLocaleProvider
|
||||||
|
|
||||||
override val showExpanded = true
|
override val showExpanded = true
|
||||||
|
|
||||||
var callback: DialPadFragment.Callback? = null
|
var callback: DialPadFragment.Callback? = null
|
||||||
@ -62,7 +67,7 @@ class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCa
|
|||||||
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, showActions)
|
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, showActions)
|
||||||
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, showActions)
|
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, showActions)
|
||||||
putBoolean(DialPadFragment.EXTRA_CURSOR_VISIBLE, false)
|
putBoolean(DialPadFragment.EXTRA_CURSOR_VISIBLE, false)
|
||||||
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country)
|
||||||
}
|
}
|
||||||
callback = DialPadFragmentCallbackWrapper(this@CallDialPadBottomSheet.callback)
|
callback = DialPadFragmentCallbackWrapper(this@CallDialPadBottomSheet.callback)
|
||||||
}.also {
|
}.also {
|
||||||
|
@ -28,7 +28,6 @@ import im.vector.app.core.extensions.addFragment
|
|||||||
import im.vector.app.core.platform.SimpleFragmentActivity
|
import im.vector.app.core.platform.SimpleFragmentActivity
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.createdirect.DirectRoomHelper
|
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||||
import im.vector.app.features.settings.VectorLocale
|
|
||||||
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
|
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -78,7 +77,7 @@ class PstnDialActivity : SimpleFragmentActivity() {
|
|||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
||||||
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
|
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
|
||||||
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country)
|
||||||
}
|
}
|
||||||
callback = object : DialPadFragment.Callback {
|
callback = object : DialPadFragment.Callback {
|
||||||
override fun onOkClicked(formatted: String?, raw: String?) {
|
override fun onOkClicked(formatted: String?, raw: String?) {
|
||||||
|
@ -59,7 +59,7 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sectionsPagerAdapter = CallTransferPagerAdapter(this)
|
sectionsPagerAdapter = CallTransferPagerAdapter(this, vectorLocale)
|
||||||
views.callTransferViewPager.adapter = sectionsPagerAdapter
|
views.callTransferViewPager.adapter = sectionsPagerAdapter
|
||||||
|
|
||||||
TabLayoutMediator(views.callTransferTabLayout, views.callTransferViewPager) { tab, position ->
|
TabLayoutMediator(views.callTransferTabLayout, views.callTransferViewPager) { tab, position ->
|
||||||
|
@ -22,12 +22,13 @@ import androidx.fragment.app.FragmentActivity
|
|||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import im.vector.app.core.extensions.toMvRxBundle
|
import im.vector.app.core.extensions.toMvRxBundle
|
||||||
import im.vector.app.features.call.dialpad.DialPadFragment
|
import im.vector.app.features.call.dialpad.DialPadFragment
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
import im.vector.app.features.userdirectory.UserListFragment
|
import im.vector.app.features.userdirectory.UserListFragment
|
||||||
import im.vector.app.features.userdirectory.UserListFragmentArgs
|
import im.vector.app.features.userdirectory.UserListFragmentArgs
|
||||||
|
|
||||||
class CallTransferPagerAdapter(
|
class CallTransferPagerAdapter(
|
||||||
private val fragmentActivity: FragmentActivity
|
private val fragmentActivity: FragmentActivity,
|
||||||
|
private val vectorLocale: VectorLocaleProvider,
|
||||||
) : FragmentStateAdapter(fragmentActivity) {
|
) : FragmentStateAdapter(fragmentActivity) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -61,7 +62,7 @@ class CallTransferPagerAdapter(
|
|||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
||||||
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, false)
|
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, false)
|
||||||
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ enum class Command(
|
|||||||
MARKDOWN("/markdown", null, "<on|off>", R.string.command_description_markdown, false, false),
|
MARKDOWN("/markdown", null, "<on|off>", R.string.command_description_markdown, false, false),
|
||||||
RAINBOW("/rainbow", null, "<message>", R.string.command_description_rainbow, false, true),
|
RAINBOW("/rainbow", null, "<message>", R.string.command_description_rainbow, false, true),
|
||||||
RAINBOW_EMOTE("/rainbowme", null, "<message>", R.string.command_description_rainbow_emote, false, true),
|
RAINBOW_EMOTE("/rainbowme", null, "<message>", R.string.command_description_rainbow_emote, false, true),
|
||||||
|
DEVTOOLS("/devtools", null, "", R.string.command_description_devtools, true, false),
|
||||||
CLEAR_SCALAR_TOKEN("/clear_scalar_token", null, "", R.string.command_description_clear_scalar_token, false, false),
|
CLEAR_SCALAR_TOKEN("/clear_scalar_token", null, "", R.string.command_description_clear_scalar_token, false, false),
|
||||||
SPOILER("/spoiler", null, "<message>", R.string.command_description_spoiler, false, true),
|
SPOILER("/spoiler", null, "<message>", R.string.command_description_spoiler, false, true),
|
||||||
SHRUG("/shrug", null, "<message>", R.string.command_description_shrug, false, true),
|
SHRUG("/shrug", null, "<message>", R.string.command_description_shrug, false, true),
|
||||||
|
@ -317,6 +317,13 @@ class CommandParser @Inject constructor() {
|
|||||||
ParsedCommand.ErrorSyntax(Command.MARKDOWN)
|
ParsedCommand.ErrorSyntax(Command.MARKDOWN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Command.DEVTOOLS.matches(slashCommand) -> {
|
||||||
|
if (messageParts.size == 1) {
|
||||||
|
ParsedCommand.DevTools
|
||||||
|
} else {
|
||||||
|
ParsedCommand.ErrorSyntax(Command.DEVTOOLS)
|
||||||
|
}
|
||||||
|
}
|
||||||
Command.CLEAR_SCALAR_TOKEN.matches(slashCommand) -> {
|
Command.CLEAR_SCALAR_TOKEN.matches(slashCommand) -> {
|
||||||
if (messageParts.size == 1) {
|
if (messageParts.size == 1) {
|
||||||
ParsedCommand.ClearScalarToken
|
ParsedCommand.ClearScalarToken
|
||||||
|
@ -60,6 +60,7 @@ sealed interface ParsedCommand {
|
|||||||
data class ChangeAvatarForRoom(val url: String) : ParsedCommand
|
data class ChangeAvatarForRoom(val url: String) : ParsedCommand
|
||||||
data class SetMarkdown(val enable: Boolean) : ParsedCommand
|
data class SetMarkdown(val enable: Boolean) : ParsedCommand
|
||||||
object ClearScalarToken : ParsedCommand
|
object ClearScalarToken : ParsedCommand
|
||||||
|
object DevTools : ParsedCommand
|
||||||
data class SendSpoiler(val message: String) : ParsedCommand
|
data class SendSpoiler(val message: String) : ParsedCommand
|
||||||
data class SendShrug(val message: CharSequence) : ParsedCommand
|
data class SendShrug(val message: CharSequence) : ParsedCommand
|
||||||
data class SendLenny(val message: CharSequence) : ParsedCommand
|
data class SendLenny(val message: CharSequence) : ParsedCommand
|
||||||
|
@ -22,7 +22,7 @@ import android.os.Build
|
|||||||
import android.os.LocaleList
|
import android.os.LocaleList
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import im.vector.app.features.settings.FontScalePreferences
|
import im.vector.app.features.settings.FontScalePreferences
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -33,21 +33,22 @@ import javax.inject.Inject
|
|||||||
*/
|
*/
|
||||||
class VectorConfiguration @Inject constructor(
|
class VectorConfiguration @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val fontScalePreferences: FontScalePreferences
|
private val fontScalePreferences: FontScalePreferences,
|
||||||
|
private val vectorLocale: VectorLocaleProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun onConfigurationChanged() {
|
fun onConfigurationChanged() {
|
||||||
if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) {
|
if (Locale.getDefault().toString() != vectorLocale.applicationLocale.toString()) {
|
||||||
Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}")
|
Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}")
|
||||||
Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}")
|
Timber.v("## onConfigurationChanged(): restore the expected value ${vectorLocale.applicationLocale}")
|
||||||
Locale.setDefault(VectorLocale.applicationLocale)
|
Locale.setDefault(vectorLocale.applicationLocale)
|
||||||
}
|
}
|
||||||
// Night mode may have changed
|
// Night mode may have changed
|
||||||
ThemeUtils.init(context)
|
ThemeUtils.init(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyToApplicationContext() {
|
fun applyToApplicationContext() {
|
||||||
val locale = VectorLocale.applicationLocale
|
val locale = vectorLocale.applicationLocale
|
||||||
val fontScale = fontScalePreferences.getResolvedFontScaleValue()
|
val fontScale = fontScalePreferences.getResolvedFontScaleValue()
|
||||||
|
|
||||||
Locale.setDefault(locale)
|
Locale.setDefault(locale)
|
||||||
@ -67,7 +68,7 @@ class VectorConfiguration @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
fun getLocalisedContext(context: Context): Context {
|
fun getLocalisedContext(context: Context): Context {
|
||||||
try {
|
try {
|
||||||
val locale = VectorLocale.applicationLocale
|
val locale = vectorLocale.applicationLocale
|
||||||
|
|
||||||
// create new configuration passing old configuration from original Context
|
// create new configuration passing old configuration from original Context
|
||||||
val configuration = Configuration(context.resources.configuration)
|
val configuration = Configuration(context.resources.configuration)
|
||||||
@ -107,7 +108,7 @@ class VectorConfiguration @Inject constructor(
|
|||||||
* @return the local status value
|
* @return the local status value
|
||||||
*/
|
*/
|
||||||
fun getHash(): String {
|
fun getHash(): String {
|
||||||
return (VectorLocale.applicationLocale.toString() +
|
return (vectorLocale.applicationLocale.toString() +
|
||||||
"_" + fontScalePreferences.getResolvedFontScaleValue().preferenceValue +
|
"_" + fontScalePreferences.getResolvedFontScaleValue().preferenceValue +
|
||||||
"_" + ThemeUtils.getApplicationTheme(context))
|
"_" + ThemeUtils.getApplicationTheme(context))
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.crypto.keysbackup.restore
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import com.airbnb.mvrx.viewModel
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
@ -27,8 +28,9 @@ import im.vector.app.core.extensions.observeEvent
|
|||||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.extensions.replaceFragment
|
import im.vector.app.core.extensions.replaceFragment
|
||||||
import im.vector.app.core.platform.SimpleFragmentActivity
|
import im.vector.app.core.platform.SimpleFragmentActivity
|
||||||
import im.vector.app.core.ui.views.KeysBackupBanner
|
|
||||||
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
||||||
|
import im.vector.app.features.workers.signout.ServerBackupStatusAction
|
||||||
|
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -46,6 +48,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
|||||||
override fun getTitleRes() = R.string.title_activity_keys_backup_restore
|
override fun getTitleRes() = R.string.title_activity_keys_backup_restore
|
||||||
|
|
||||||
private lateinit var viewModel: KeysBackupRestoreSharedViewModel
|
private lateinit var viewModel: KeysBackupRestoreSharedViewModel
|
||||||
|
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
hideWaitingView()
|
hideWaitingView()
|
||||||
@ -95,7 +98,8 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
|||||||
}
|
}
|
||||||
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
|
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
|
||||||
viewModel.keyVersionResult.value?.version?.let {
|
viewModel.keyVersionResult.value?.version?.let {
|
||||||
KeysBackupBanner.onRecoverDoneForVersion(this, it)
|
// Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version.
|
||||||
|
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnRecoverDoneForVersion(it))
|
||||||
}
|
}
|
||||||
replaceFragment(views.container, KeysBackupRestoreSuccessFragment::class.java, allowStateLoss = true)
|
replaceFragment(views.container, KeysBackupRestoreSuccessFragment::class.java, allowStateLoss = true)
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,10 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.extensions.hidePassword
|
import im.vector.app.core.extensions.hidePassword
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentKeysBackupSetupStep2Binding
|
import im.vector.app.databinding.FragmentKeysBackupSetupStep2Binding
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class KeysBackupSetupStep2Fragment :
|
class KeysBackupSetupStep2Fragment :
|
||||||
@ -43,6 +44,8 @@ class KeysBackupSetupStep2Fragment :
|
|||||||
|
|
||||||
private val zxcvbn = Zxcvbn()
|
private val zxcvbn = Zxcvbn()
|
||||||
|
|
||||||
|
@Inject lateinit var vectorLocale: VectorLocaleProvider
|
||||||
|
|
||||||
private fun onPassphraseChanged() {
|
private fun onPassphraseChanged() {
|
||||||
viewModel.passphrase.value = views.keysBackupSetupStep2PassphraseEnterEdittext.text.toString()
|
viewModel.passphrase.value = views.keysBackupSetupStep2PassphraseEnterEdittext.text.toString()
|
||||||
viewModel.confirmPassphraseError.value = null
|
viewModel.confirmPassphraseError.value = null
|
||||||
@ -78,12 +81,12 @@ class KeysBackupSetupStep2Fragment :
|
|||||||
views.keysBackupSetupStep2PassphraseStrengthLevel.strength = score
|
views.keysBackupSetupStep2PassphraseStrengthLevel.strength = score
|
||||||
|
|
||||||
if (score in 1..3) {
|
if (score in 1..3) {
|
||||||
val warning = strength.feedback?.getWarning(VectorLocale.applicationLocale)
|
val warning = strength.feedback?.getWarning(vectorLocale.applicationLocale)
|
||||||
if (warning != null) {
|
if (warning != null) {
|
||||||
views.keysBackupSetupStep2PassphraseEnterTil.error = warning
|
views.keysBackupSetupStep2PassphraseEnterTil.error = warning
|
||||||
}
|
}
|
||||||
|
|
||||||
val suggestions = strength.feedback?.getSuggestions(VectorLocale.applicationLocale)
|
val suggestions = strength.feedback?.getSuggestions(vectorLocale.applicationLocale)
|
||||||
if (suggestions != null) {
|
if (suggestions != null) {
|
||||||
views.keysBackupSetupStep2PassphraseEnterTil.error = suggestions.firstOrNull()
|
views.keysBackupSetupStep2PassphraseEnterTil.error = suggestions.firstOrNull()
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,13 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding
|
import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
import im.vector.lib.core.utils.flow.throttleFirst
|
import im.vector.lib.core.utils.flow.throttleFirst
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.android.widget.editorActionEvents
|
import reactivecircus.flowbinding.android.widget.editorActionEvents
|
||||||
import reactivecircus.flowbinding.android.widget.textChanges
|
import reactivecircus.flowbinding.android.widget.textChanges
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootstrapEnterPassphraseFragment :
|
class BootstrapEnterPassphraseFragment :
|
||||||
@ -43,6 +44,8 @@ class BootstrapEnterPassphraseFragment :
|
|||||||
return FragmentBootstrapEnterPassphraseBinding.inflate(inflater, container, false)
|
return FragmentBootstrapEnterPassphraseBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject lateinit var vectorLocale: VectorLocaleProvider
|
||||||
|
|
||||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@ -105,8 +108,8 @@ class BootstrapEnterPassphraseFragment :
|
|||||||
views.ssssPassphraseSecurityProgress.strength = score
|
views.ssssPassphraseSecurityProgress.strength = score
|
||||||
if (score in 1..3) {
|
if (score in 1..3) {
|
||||||
val hint =
|
val hint =
|
||||||
strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() }
|
strength.feedback?.getWarning(vectorLocale.applicationLocale)?.takeIf { it.isNotBlank() }
|
||||||
?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull()
|
?: strength.feedback?.getSuggestions(vectorLocale.applicationLocale)?.firstOrNull()
|
||||||
if (hint != null && hint != views.ssssPassphraseEnterTil.error.toString()) {
|
if (hint != null && hint != views.ssssPassphraseEnterTil.error.toString()) {
|
||||||
views.ssssPassphraseEnterTil.error = hint
|
views.ssssPassphraseEnterTil.error = hint
|
||||||
}
|
}
|
||||||
|
@ -17,22 +17,25 @@
|
|||||||
package im.vector.app.features.disclaimer
|
package im.vector.app.features.disclaimer
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||||
import im.vector.app.features.settings.VectorSettingsUrls
|
import im.vector.app.features.settings.VectorSettingsUrls
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
// Increase this value to show again the disclaimer dialog after an upgrade of the application
|
// Increase this value to show again the disclaimer dialog after an upgrade of the application
|
||||||
private const val CURRENT_DISCLAIMER_VALUE = 2
|
private const val CURRENT_DISCLAIMER_VALUE = 2
|
||||||
|
|
||||||
const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE"
|
const val SHARED_PREF_KEY = "LAST_DISCLAIMER_VERSION_VALUE"
|
||||||
|
|
||||||
|
class DisclaimerDialog @Inject constructor(
|
||||||
|
@DefaultPreferences
|
||||||
|
private val sharedPrefs: SharedPreferences,
|
||||||
|
) {
|
||||||
fun showDisclaimerDialog(activity: Activity) {
|
fun showDisclaimerDialog(activity: Activity) {
|
||||||
val sharedPrefs = DefaultSharedPreferences.getInstance(activity)
|
|
||||||
|
|
||||||
if (sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE) {
|
if (sharedPrefs.getInt(SHARED_PREF_KEY, 0) < CURRENT_DISCLAIMER_VALUE) {
|
||||||
sharedPrefs.edit {
|
sharedPrefs.edit {
|
||||||
putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE)
|
putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE)
|
||||||
@ -51,10 +54,9 @@ fun showDisclaimerDialog(activity: Activity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doNotShowDisclaimerDialog(context: Context) {
|
fun doNotShowDisclaimerDialog() {
|
||||||
val sharedPrefs = DefaultSharedPreferences.getInstance(context)
|
|
||||||
|
|
||||||
sharedPrefs.edit {
|
sharedPrefs.edit {
|
||||||
putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE)
|
putInt(SHARED_PREF_KEY, CURRENT_DISCLAIMER_VALUE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -56,7 +56,7 @@ import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewMode
|
|||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.analytics.plan.ViewRoom
|
import im.vector.app.features.analytics.plan.ViewRoom
|
||||||
import im.vector.app.features.crypto.recover.SetupMode
|
import im.vector.app.features.crypto.recover.SetupMode
|
||||||
import im.vector.app.features.disclaimer.showDisclaimerDialog
|
import im.vector.app.features.disclaimer.DisclaimerDialog
|
||||||
import im.vector.app.features.home.room.list.actions.RoomListSharedAction
|
import im.vector.app.features.home.room.list.actions.RoomListSharedAction
|
||||||
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
|
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
|
||||||
import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment
|
import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment
|
||||||
@ -141,6 +141,7 @@ class HomeActivity :
|
|||||||
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
|
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
|
||||||
@Inject lateinit var fcmHelper: FcmHelper
|
@Inject lateinit var fcmHelper: FcmHelper
|
||||||
@Inject lateinit var nightlyProxy: NightlyProxy
|
@Inject lateinit var nightlyProxy: NightlyProxy
|
||||||
|
@Inject lateinit var disclaimerDialog: DisclaimerDialog
|
||||||
|
|
||||||
private var isNewAppLayoutEnabled: Boolean = false // delete once old app layout is removed
|
private var isNewAppLayoutEnabled: Boolean = false // delete once old app layout is removed
|
||||||
|
|
||||||
@ -570,7 +571,7 @@ class HomeActivity :
|
|||||||
.setNegativeButton(R.string.no) { _, _ -> bugReporter.deleteCrashFile() }
|
.setNegativeButton(R.string.no) { _, _ -> bugReporter.deleteCrashFile() }
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
showDisclaimerDialog(this)
|
disclaimerDialog.showDisclaimerDialog(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force remote backup state update to update the banner if needed
|
// Force remote backup state update to update the banner if needed
|
||||||
|
@ -51,11 +51,12 @@ import im.vector.app.features.home.room.list.RoomListParams
|
|||||||
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
||||||
import im.vector.app.features.popup.PopupAlertManager
|
import im.vector.app.features.popup.PopupAlertManager
|
||||||
import im.vector.app.features.popup.VerificationVectorAlert
|
import im.vector.app.features.popup.VerificationVectorAlert
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import im.vector.app.features.workers.signout.BannerState
|
import im.vector.app.features.workers.signout.BannerState
|
||||||
|
import im.vector.app.features.workers.signout.ServerBackupStatusAction
|
||||||
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
|
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
@ -75,6 +76,7 @@ class HomeDetailFragment :
|
|||||||
@Inject lateinit var callManager: WebRtcCallManager
|
@Inject lateinit var callManager: WebRtcCallManager
|
||||||
@Inject lateinit var vectorPreferences: VectorPreferences
|
@Inject lateinit var vectorPreferences: VectorPreferences
|
||||||
@Inject lateinit var spaceStateHandler: SpaceStateHandler
|
@Inject lateinit var spaceStateHandler: SpaceStateHandler
|
||||||
|
@Inject lateinit var vectorLocale: VectorLocaleProvider
|
||||||
|
|
||||||
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
||||||
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
||||||
@ -288,13 +290,15 @@ class HomeDetailFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupKeysBackupBanner() {
|
private fun setupKeysBackupBanner() {
|
||||||
|
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerDisplayed)
|
||||||
serverBackupStatusViewModel
|
serverBackupStatusViewModel
|
||||||
.onEach {
|
.onEach {
|
||||||
when (val banState = it.bannerState.invoke()) {
|
when (val banState = it.bannerState.invoke()) {
|
||||||
is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
|
is BannerState.Setup,
|
||||||
BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
|
BannerState.BackingUp,
|
||||||
null,
|
BannerState.Hidden -> views.homeKeysBackupBanner.render(banState, false)
|
||||||
BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
|
null -> views.homeKeysBackupBanner.render(BannerState.Hidden, false)
|
||||||
|
else -> Unit /* No op? */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
views.homeKeysBackupBanner.delegate = this
|
views.homeKeysBackupBanner.delegate = this
|
||||||
@ -378,7 +382,7 @@ class HomeDetailFragment :
|
|||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
||||||
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
|
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
|
||||||
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
putString(DialPadFragment.EXTRA_REGION_CODE, vectorLocale.applicationLocale.country)
|
||||||
}
|
}
|
||||||
applyCallback()
|
applyCallback()
|
||||||
}
|
}
|
||||||
@ -401,6 +405,10 @@ class HomeDetailFragment :
|
|||||||
* KeysBackupBanner Listener
|
* KeysBackupBanner Listener
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
override fun onCloseClicked() {
|
||||||
|
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setupKeysBackup() {
|
override fun setupKeysBackup() {
|
||||||
navigator.openKeysBackupSetup(requireActivity(), false)
|
navigator.openKeysBackupSetup(requireActivity(), false)
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ import im.vector.app.features.settings.VectorPreferences
|
|||||||
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
||||||
import im.vector.app.features.spaces.SpaceListBottomSheet
|
import im.vector.app.features.spaces.SpaceListBottomSheet
|
||||||
import im.vector.app.features.workers.signout.BannerState
|
import im.vector.app.features.workers.signout.BannerState
|
||||||
|
import im.vector.app.features.workers.signout.ServerBackupStatusAction
|
||||||
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
|
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -300,13 +301,15 @@ class NewHomeDetailFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupKeysBackupBanner() {
|
private fun setupKeysBackupBanner() {
|
||||||
|
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerDisplayed)
|
||||||
serverBackupStatusViewModel
|
serverBackupStatusViewModel
|
||||||
.onEach {
|
.onEach {
|
||||||
when (val banState = it.bannerState.invoke()) {
|
when (val banState = it.bannerState.invoke()) {
|
||||||
is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
|
is BannerState.Setup,
|
||||||
BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
|
BannerState.BackingUp,
|
||||||
null,
|
BannerState.Hidden -> views.homeKeysBackupBanner.render(banState, false)
|
||||||
BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
|
null -> views.homeKeysBackupBanner.render(BannerState.Hidden, false)
|
||||||
|
else -> Unit /* No op? */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
views.homeKeysBackupBanner.delegate = this
|
views.homeKeysBackupBanner.delegate = this
|
||||||
@ -348,6 +351,10 @@ class NewHomeDetailFragment :
|
|||||||
* KeysBackupBanner Listener
|
* KeysBackupBanner Listener
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
override fun onCloseClicked() {
|
||||||
|
serverBackupStatusViewModel.handle(ServerBackupStatusAction.OnBannerClosed)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setupKeysBackup() {
|
override fun setupKeysBackup() {
|
||||||
navigator.openKeysBackupSetup(requireActivity(), false)
|
navigator.openKeysBackupSetup(requireActivity(), false)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||||||
import android.content.pm.ShortcutInfo
|
import android.content.pm.ShortcutInfo
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
@ -32,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
|||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||||
private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||||
private const val adaptiveIconSizeDp = 108
|
private const val adaptiveIconSizeDp = 108
|
||||||
private const val adaptiveIconOuterSidesDp = 18
|
private const val adaptiveIconOuterSidesDp = 18
|
||||||
|
@ -1124,6 +1124,7 @@ class TimelineFragment :
|
|||||||
.findViewById<ImageView>(R.id.action_view_icon_image)
|
.findViewById<ImageView>(R.id.action_view_icon_image)
|
||||||
.setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary))
|
.setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary))
|
||||||
actionView.findViewById<TextView>(R.id.cart_badge).setTextOrHide("$widgetsCount")
|
actionView.findViewById<TextView>(R.id.cart_badge).setTextOrHide("$widgetsCount")
|
||||||
|
@Suppress("AlwaysShowAction")
|
||||||
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1810,6 +1811,9 @@ class TimelineFragment :
|
|||||||
dismissLoadingDialog()
|
dismissLoadingDialog()
|
||||||
views.composerLayout.setTextIfDifferent("")
|
views.composerLayout.setTextIfDifferent("")
|
||||||
when (parsedCommand) {
|
when (parsedCommand) {
|
||||||
|
is ParsedCommand.DevTools -> {
|
||||||
|
navigator.openDevTools(requireContext(), timelineArgs.roomId)
|
||||||
|
}
|
||||||
is ParsedCommand.SetMarkdown -> {
|
is ParsedCommand.SetMarkdown -> {
|
||||||
showSnackWithMessage(getString(if (parsedCommand.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
|
showSnackWithMessage(getString(if (parsedCommand.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
|
||||||
}
|
}
|
||||||
|
@ -255,6 +255,10 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
is ParsedCommand.SetUserPowerLevel -> {
|
is ParsedCommand.SetUserPowerLevel -> {
|
||||||
handleSetUserPowerLevel(parsedCommand)
|
handleSetUserPowerLevel(parsedCommand)
|
||||||
}
|
}
|
||||||
|
is ParsedCommand.DevTools -> {
|
||||||
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
|
||||||
|
popDraft()
|
||||||
|
}
|
||||||
is ParsedCommand.ClearScalarToken -> {
|
is ParsedCommand.ClearScalarToken -> {
|
||||||
// TODO
|
// TODO
|
||||||
_viewEvents.post(MessageComposerViewEvents.SlashCommandNotImplemented)
|
_viewEvents.post(MessageComposerViewEvents.SlashCommandNotImplemented)
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.list.home
|
package im.vector.app.features.home.room.list.home
|
||||||
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import com.airbnb.epoxy.EpoxyModel
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||||
import im.vector.app.core.platform.StateView
|
import im.vector.app.core.platform.StateView
|
||||||
@ -76,13 +75,6 @@ class HomeFilteredRoomsController @Inject constructor(
|
|||||||
this.emptyStateData = state
|
this.emptyStateData = state
|
||||||
}
|
}
|
||||||
|
|
||||||
fun submitPagedList(newList: PagedList<RoomSummary>) {
|
|
||||||
submitList(newList)
|
|
||||||
if (newList.isEmpty()) {
|
|
||||||
requestForcedModelBuild()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
|
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
|
||||||
return if (item == null) {
|
return if (item == null) {
|
||||||
val host = this
|
val host = this
|
||||||
|
@ -152,14 +152,12 @@ class HomeRoomListFragment :
|
|||||||
headersController.submitData(it)
|
headersController.submitData(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
roomListViewModel.onEach(HomeRoomListViewState::roomsPagedList) { roomsList ->
|
roomListViewModel.roomsLivePagedList.observe(viewLifecycleOwner) { roomsList ->
|
||||||
roomsList?.let {
|
roomsController.submitList(roomsList)
|
||||||
roomsController.submitPagedList(it)
|
if (roomsList.isEmpty()) {
|
||||||
if (it.isEmpty()) {
|
|
||||||
roomsController.requestForcedModelBuild()
|
roomsController.requestForcedModelBuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
roomListViewModel.emptyStateFlow.onEach { emptyStateOptional ->
|
roomListViewModel.emptyStateFlow.onEach { emptyStateOptional ->
|
||||||
roomsController.submitEmptyStateData(emptyStateOptional.getOrNull())
|
roomsController.submitEmptyStateData(emptyStateOptional.getOrNull())
|
||||||
|
@ -17,8 +17,11 @@
|
|||||||
package im.vector.app.features.home.room.list.home
|
package im.vector.app.features.home.room.list.home
|
||||||
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
|
import arrow.core.Option
|
||||||
import arrow.core.toOption
|
import arrow.core.toOption
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
@ -35,7 +38,6 @@ import im.vector.app.core.resources.StringProvider
|
|||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
|
import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
@ -69,7 +71,6 @@ import org.matrix.android.sdk.api.session.room.state.isPublic
|
|||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import org.matrix.android.sdk.flow.flow
|
import org.matrix.android.sdk.flow.flow
|
||||||
import java.util.concurrent.CancellationException
|
|
||||||
|
|
||||||
class HomeRoomListViewModel @AssistedInject constructor(
|
class HomeRoomListViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: HomeRoomListViewState,
|
@Assisted initialState: HomeRoomListViewState,
|
||||||
@ -87,19 +88,24 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
companion object : MavericksViewModelFactory<HomeRoomListViewModel, HomeRoomListViewState> by hiltMavericksViewModelFactory()
|
companion object : MavericksViewModelFactory<HomeRoomListViewModel, HomeRoomListViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
|
private var roomsFlow: Flow<Option<RoomSummary>>? = null
|
||||||
private val pagedListConfig = PagedList.Config.Builder()
|
private val pagedListConfig = PagedList.Config.Builder()
|
||||||
.setPageSize(10)
|
.setPageSize(10)
|
||||||
.setInitialLoadSizeHint(20)
|
.setInitialLoadSizeHint(20)
|
||||||
.setEnablePlaceholders(true)
|
.setEnablePlaceholders(true)
|
||||||
.setPrefetchDistance(10)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
private val _roomsLivePagedList = MutableLiveData<PagedList<RoomSummary>>()
|
||||||
|
val roomsLivePagedList: LiveData<PagedList<RoomSummary>> = _roomsLivePagedList
|
||||||
|
|
||||||
|
private val internalPagedListObserver = Observer<PagedList<RoomSummary>> {
|
||||||
|
_roomsLivePagedList.postValue(it)
|
||||||
|
}
|
||||||
|
|
||||||
private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL
|
private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL
|
||||||
private val _emptyStateFlow = MutableSharedFlow<Optional<StateView.State.Empty>>(replay = 1)
|
private val _emptyStateFlow = MutableSharedFlow<Optional<StateView.State.Empty>>(replay = 1)
|
||||||
val emptyStateFlow = _emptyStateFlow.asSharedFlow()
|
val emptyStateFlow = _emptyStateFlow.asSharedFlow()
|
||||||
|
|
||||||
private var roomsFlowJob: Job? = null
|
|
||||||
|
|
||||||
private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null
|
private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -223,6 +229,8 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun observeRooms() = viewModelScope.launch {
|
private fun observeRooms() = viewModelScope.launch {
|
||||||
|
filteredPagedRoomSummariesLive?.livePagedList?.removeObserver(internalPagedListObserver)
|
||||||
|
|
||||||
val builder = RoomSummaryQueryParams.Builder().also {
|
val builder = RoomSummaryQueryParams.Builder().also {
|
||||||
it.memberships = listOf(Membership.JOIN)
|
it.memberships = listOf(Membership.JOIN)
|
||||||
}
|
}
|
||||||
@ -233,7 +241,6 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
|||||||
} else {
|
} else {
|
||||||
RoomSortOrder.ACTIVITY
|
RoomSortOrder.ACTIVITY
|
||||||
}
|
}
|
||||||
|
|
||||||
val liveResults = session.roomService().getFilteredPagedRoomSummariesLive(
|
val liveResults = session.roomService().getFilteredPagedRoomSummariesLive(
|
||||||
params,
|
params,
|
||||||
pagedListConfig,
|
pagedListConfig,
|
||||||
@ -249,21 +256,15 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
.onEach { selectedSpaceOption ->
|
.onEach { selectedSpaceOption ->
|
||||||
val selectedSpace = selectedSpaceOption.orNull()
|
val selectedSpace = selectedSpaceOption.orNull()
|
||||||
liveResults.queryParams = liveResults.queryParams.copy(
|
filteredPagedRoomSummariesLive?.queryParams = liveResults.queryParams.copy(
|
||||||
spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter()
|
spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter()
|
||||||
)
|
)
|
||||||
emitEmptyState()
|
emitEmptyState()
|
||||||
}
|
}
|
||||||
|
.also { roomsFlow = it }
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
roomsFlowJob?.cancel(CancellationException())
|
liveResults.livePagedList.observeForever(internalPagedListObserver)
|
||||||
|
|
||||||
roomsFlowJob = liveResults.livePagedList
|
|
||||||
.asFlow()
|
|
||||||
.onEach {
|
|
||||||
setState { copy(roomsPagedList = it) }
|
|
||||||
}
|
|
||||||
.launchIn(viewModelScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeOrderPreferences() {
|
private fun observeOrderPreferences() {
|
||||||
@ -344,6 +345,11 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
filteredPagedRoomSummariesLive?.livePagedList?.removeObserver(internalPagedListObserver)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleChangeRoomFilter(newFilter: HomeRoomFilter) {
|
private fun handleChangeRoomFilter(newFilter: HomeRoomFilter) {
|
||||||
if (currentFilter == newFilter) {
|
if (currentFilter == newFilter) {
|
||||||
return
|
return
|
||||||
|
@ -16,14 +16,11 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.list.home
|
package im.vector.app.features.home.room.list.home
|
||||||
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import im.vector.app.core.platform.StateView
|
import im.vector.app.core.platform.StateView
|
||||||
import im.vector.app.features.home.room.list.home.header.RoomsHeadersData
|
import im.vector.app.features.home.room.list.home.header.RoomsHeadersData
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
|
||||||
|
|
||||||
data class HomeRoomListViewState(
|
data class HomeRoomListViewState(
|
||||||
val state: StateView.State = StateView.State.Content,
|
val state: StateView.State = StateView.State.Content,
|
||||||
val headersData: RoomsHeadersData = RoomsHeadersData(),
|
val headersData: RoomsHeadersData = RoomsHeadersData(),
|
||||||
val roomsPagedList: PagedList<RoomSummary>? = null,
|
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
@ -16,16 +16,22 @@
|
|||||||
|
|
||||||
package im.vector.app.features.homeserver
|
package im.vector.app.features.homeserver
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object to store and retrieve home and identity server urls.
|
* Object to store and retrieve home and identity server urls.
|
||||||
*/
|
*/
|
||||||
object ServerUrlsRepository {
|
class ServerUrlsRepository @Inject constructor(
|
||||||
|
@DefaultPreferences
|
||||||
|
private val sharedPreferences: SharedPreferences,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
// Keys used to store default servers urls from the referrer
|
// Keys used to store default servers urls from the referrer
|
||||||
private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url"
|
private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url"
|
||||||
private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url"
|
private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url"
|
||||||
@ -33,12 +39,13 @@ object ServerUrlsRepository {
|
|||||||
// Keys used to store current homeserver url and identity url
|
// Keys used to store current homeserver url and identity url
|
||||||
const val HOME_SERVER_URL_PREF = "home_server_url"
|
const val HOME_SERVER_URL_PREF = "home_server_url"
|
||||||
const val IDENTITY_SERVER_URL_PREF = "identity_server_url"
|
const val IDENTITY_SERVER_URL_PREF = "identity_server_url"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save home and identity sever urls received by the Referrer receiver.
|
* Save home and identity sever urls received by the Referrer receiver.
|
||||||
*/
|
*/
|
||||||
fun setDefaultUrlsFromReferrer(context: Context, homeServerUrl: String, identityServerUrl: String) {
|
fun setDefaultUrlsFromReferrer(homeServerUrl: String, identityServerUrl: String) {
|
||||||
DefaultSharedPreferences.getInstance(context)
|
sharedPreferences
|
||||||
.edit {
|
.edit {
|
||||||
if (homeServerUrl.isNotEmpty()) {
|
if (homeServerUrl.isNotEmpty()) {
|
||||||
putString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, homeServerUrl)
|
putString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, homeServerUrl)
|
||||||
@ -53,8 +60,8 @@ object ServerUrlsRepository {
|
|||||||
/**
|
/**
|
||||||
* Save home and identity sever urls entered by the user. May be custom or default value.
|
* Save home and identity sever urls entered by the user. May be custom or default value.
|
||||||
*/
|
*/
|
||||||
fun saveServerUrls(context: Context, homeServerUrl: String, identityServerUrl: String) {
|
fun saveServerUrls(homeServerUrl: String, identityServerUrl: String) {
|
||||||
DefaultSharedPreferences.getInstance(context)
|
sharedPreferences
|
||||||
.edit {
|
.edit {
|
||||||
putString(HOME_SERVER_URL_PREF, homeServerUrl)
|
putString(HOME_SERVER_URL_PREF, homeServerUrl)
|
||||||
putString(IDENTITY_SERVER_URL_PREF, identityServerUrl)
|
putString(IDENTITY_SERVER_URL_PREF, identityServerUrl)
|
||||||
@ -64,14 +71,12 @@ object ServerUrlsRepository {
|
|||||||
/**
|
/**
|
||||||
* Return last used homeserver url, or the default one from referrer or the default one from resources.
|
* Return last used homeserver url, or the default one from referrer or the default one from resources.
|
||||||
*/
|
*/
|
||||||
fun getLastHomeServerUrl(context: Context): String {
|
fun getLastHomeServerUrl(): String {
|
||||||
val prefs = DefaultSharedPreferences.getInstance(context)
|
return sharedPreferences.getString(
|
||||||
|
|
||||||
return prefs.getString(
|
|
||||||
HOME_SERVER_URL_PREF,
|
HOME_SERVER_URL_PREF,
|
||||||
prefs.getString(
|
sharedPreferences.getString(
|
||||||
DEFAULT_REFERRER_HOME_SERVER_URL_PREF,
|
DEFAULT_REFERRER_HOME_SERVER_URL_PREF,
|
||||||
getDefaultHomeServerUrl(context)
|
getDefaultHomeServerUrl()
|
||||||
)!!
|
)!!
|
||||||
)!!
|
)!!
|
||||||
}
|
}
|
||||||
@ -79,10 +84,10 @@ object ServerUrlsRepository {
|
|||||||
/**
|
/**
|
||||||
* Return true if url is the default homeserver url form resources.
|
* Return true if url is the default homeserver url form resources.
|
||||||
*/
|
*/
|
||||||
fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context)
|
fun isDefaultHomeServerUrl(url: String) = url == getDefaultHomeServerUrl()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return default homeserver url from resources.
|
* Return default homeserver url from resources.
|
||||||
*/
|
*/
|
||||||
fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.matrix_org_server_url)
|
fun getDefaultHomeServerUrl() = stringProvider.getString(R.string.matrix_org_server_url)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.lifecycle
|
package im.vector.app.features.lifecycle
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
@ -91,8 +90,6 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager
|
|||||||
*
|
*
|
||||||
* @return true if an app task is corrupted by a potentially malicious activity
|
* @return true if an app task is corrupted by a potentially malicious activity
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private suspend fun isTaskCorrupted(activity: Activity): Boolean = withContext(Dispatchers.Default) {
|
private suspend fun isTaskCorrupted(activity: Activity): Boolean = withContext(Dispatchers.Default) {
|
||||||
val context = activity.applicationContext
|
val context = activity.applicationContext
|
||||||
val packageManager: PackageManager = context.packageManager
|
val packageManager: PackageManager = context.packageManager
|
||||||
@ -120,6 +117,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager
|
|||||||
// This was present in ActivityManager.RunningTaskInfo class since API level 1!
|
// This was present in ActivityManager.RunningTaskInfo class since API level 1!
|
||||||
// and it is inherited from TaskInfo since Android Q (API level 29).
|
// and it is inherited from TaskInfo since Android Q (API level 29).
|
||||||
// API 29 changes : https://developer.android.com/sdk/api_diff/29/changes/android.app.ActivityManager.RunningTaskInfo
|
// API 29 changes : https://developer.android.com/sdk/api_diff/29/changes/android.app.ActivityManager.RunningTaskInfo
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
manager.getRunningTasks(10).any { runningTaskInfo ->
|
manager.getRunningTasks(10).any { runningTaskInfo ->
|
||||||
runningTaskInfo.topActivity?.let {
|
runningTaskInfo.topActivity?.let {
|
||||||
// Check whether the activity task affinity matches with app task affinity.
|
// Check whether the activity task affinity matches with app task affinity.
|
||||||
|
@ -144,7 +144,6 @@ class LoginCaptchaFragment :
|
|||||||
// runOnUiThread(Runnable { finish() })
|
// runOnUiThread(Runnable { finish() })
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
|
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
|
||||||
super.onReceivedHttpError(view, request, errorResponse)
|
super.onReceivedHttpError(view, request, errorResponse)
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@ -34,6 +33,7 @@ import android.text.Spannable
|
|||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@ -102,6 +102,7 @@ class NotificationUtils @Inject constructor(
|
|||||||
const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
|
const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
|
||||||
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
|
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
|
||||||
|
|
||||||
|
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||||
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
|
||||||
fun openSystemSettingsForSilentCategory(fragment: Fragment) {
|
fun openSystemSettingsForSilentCategory(fragment: Fragment) {
|
||||||
@ -126,7 +127,6 @@ class NotificationUtils @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Create notification channels.
|
* Create notification channels.
|
||||||
*/
|
*/
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
|
||||||
fun createNotificationChannels() {
|
fun createNotificationChannels() {
|
||||||
if (!supportNotificationChannels()) {
|
if (!supportNotificationChannels()) {
|
||||||
return
|
return
|
||||||
@ -218,7 +218,6 @@ class NotificationUtils @Inject constructor(
|
|||||||
* @param withProgress true to show indeterminate progress on the notification
|
* @param withProgress true to show indeterminate progress on the notification
|
||||||
* @return the polling thread listener notification
|
* @return the polling thread listener notification
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
|
fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
|
||||||
// build the pending intent go to the home screen if this is clicked.
|
// build the pending intent go to the home screen if this is clicked.
|
||||||
val i = HomeActivity.newIntent(context, firstStartMainActivity = false)
|
val i = HomeActivity.newIntent(context, firstStartMainActivity = false)
|
||||||
@ -287,7 +286,6 @@ class NotificationUtils @Inject constructor(
|
|||||||
* @param fromBg true if the app is in background when posting the notification
|
* @param fromBg true if the app is in background when posting the notification
|
||||||
* @return the call notification.
|
* @return the call notification.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
fun buildIncomingCallNotification(
|
fun buildIncomingCallNotification(
|
||||||
call: WebRtcCall,
|
call: WebRtcCall,
|
||||||
title: String,
|
title: String,
|
||||||
@ -420,7 +418,6 @@ class NotificationUtils @Inject constructor(
|
|||||||
* @param title title of the notification
|
* @param title title of the notification
|
||||||
* @return the call notification.
|
* @return the call notification.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
fun buildPendingCallNotification(
|
fun buildPendingCallNotification(
|
||||||
call: WebRtcCall,
|
call: WebRtcCall,
|
||||||
title: String
|
title: String
|
||||||
@ -966,6 +963,7 @@ class NotificationUtils @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("LaunchActivityFromNotification")
|
||||||
fun displayDiagnosticNotification() {
|
fun displayDiagnosticNotification() {
|
||||||
val testActionIntent = Intent(context, TestNotificationReceiver::class.java)
|
val testActionIntent = Intent(context, TestNotificationReceiver::class.java)
|
||||||
testActionIntent.action = actionIds.diagnostic
|
testActionIntent.action = actionIds.diagnostic
|
||||||
|
@ -92,7 +92,6 @@ class CaptchaWebview @Inject constructor(
|
|||||||
Timber.e("## onError() : $errorMessage")
|
Timber.e("## onError() : $errorMessage")
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
|
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
|
||||||
super.onReceivedHttpError(view, request, errorResponse)
|
super.onReceivedHttpError(view, request, errorResponse)
|
||||||
when {
|
when {
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.pin.lockscreen.biometrics
|
package im.vector.app.features.pin.lockscreen.biometrics
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
@ -156,7 +155,6 @@ class BiometricHelper @AssistedInject constructor(
|
|||||||
return authenticate(activity)
|
return authenticate(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun authenticateInternal(
|
private fun authenticateInternal(
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.pin.lockscreen.crypto
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
@ -55,7 +54,6 @@ class KeyStoreCrypto @AssistedInject constructor(
|
|||||||
* Ensures a [Key] for the [alias] exists and validates it.
|
* Ensures a [Key] for the [alias] exists and validates it.
|
||||||
* @throws KeyPermanentlyInvalidatedException if key is not valid.
|
* @throws KeyPermanentlyInvalidatedException if key is not valid.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@Throws(KeyPermanentlyInvalidatedException::class)
|
@Throws(KeyPermanentlyInvalidatedException::class)
|
||||||
fun ensureKey() = secretStoringUtils.ensureKey(alias).also {
|
fun ensureKey() = secretStoringUtils.ensureKey(alias).also {
|
||||||
// Check validity of Key by initializing an encryption Cipher
|
// Check validity of Key by initializing an encryption Cipher
|
||||||
@ -109,10 +107,9 @@ class KeyStoreCrypto @AssistedInject constructor(
|
|||||||
/**
|
/**
|
||||||
* Check if the key associated with the [alias] is valid.
|
* Check if the key associated with the [alias] is valid.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
fun hasValidKey(): Boolean {
|
fun hasValidKey(): Boolean {
|
||||||
val keyExists = hasKey()
|
val keyExists = hasKey()
|
||||||
return if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && keyExists) {
|
return if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) && keyExists) {
|
||||||
val initializedKey = tryOrNull("Error validating lockscreen system key.") { ensureKey() }
|
val initializedKey = tryOrNull("Error validating lockscreen system key.") { ensureKey() }
|
||||||
initializedKey != null
|
initializedKey != null
|
||||||
} else {
|
} else {
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.pin.lockscreen.crypto
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
|
import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.migrations.MissingSystemKeyMigrator
|
import im.vector.app.features.pin.lockscreen.crypto.migrations.MissingSystemKeyMigrator
|
||||||
@ -36,14 +35,13 @@ class LockScreenKeysMigrator @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Performs any needed migrations in order.
|
* Performs any needed migrations in order.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
suspend fun migrateIfNeeded() {
|
suspend fun migrateIfNeeded() {
|
||||||
if (legacyPinCodeMigrator.isMigrationNeeded()) {
|
if (legacyPinCodeMigrator.isMigrationNeeded()) {
|
||||||
legacyPinCodeMigrator.migrate()
|
legacyPinCodeMigrator.migrate()
|
||||||
missingSystemKeyMigrator.migrateIfNeeded()
|
missingSystemKeyMigrator.migrateIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (systemKeyV1Migrator.isMigrationNeeded() && versionProvider.get() >= Build.VERSION_CODES.M) {
|
if (systemKeyV1Migrator.isMigrationNeeded() && versionProvider.isAtLeast(Build.VERSION_CODES.M)) {
|
||||||
systemKeyV1Migrator.migrate()
|
systemKeyV1Migrator.migrate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.pin.lockscreen.crypto.migrations
|
package im.vector.app.features.pin.lockscreen.crypto.migrations
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
|
import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
|
||||||
import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias
|
import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias
|
||||||
@ -38,9 +37,9 @@ class MissingSystemKeyMigrator @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* If user had biometric auth enabled, ensure system key exists, creating one if needed.
|
* If user had biometric auth enabled, ensure system key exists, creating one if needed.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
fun migrateIfNeeded() {
|
fun migrateIfNeeded() {
|
||||||
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && vectorPreferences.useBiometricsToUnlock()) {
|
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) &&
|
||||||
|
vectorPreferences.useBiometricsToUnlock()) {
|
||||||
val systemKeyStoreCrypto = keystoreCryptoFactory.provide(systemKeyAlias, true)
|
val systemKeyStoreCrypto = keystoreCryptoFactory.provide(systemKeyAlias, true)
|
||||||
runCatching {
|
runCatching {
|
||||||
systemKeyStoreCrypto.ensureKey()
|
systemKeyStoreCrypto.ensureKey()
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.pin.lockscreen.ui
|
package im.vector.app.features.pin.lockscreen.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.KeyguardManager
|
import android.app.KeyguardManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
@ -139,12 +138,12 @@ class LockScreenViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private fun showBiometricPrompt(activity: FragmentActivity) = flow {
|
private fun showBiometricPrompt(activity: FragmentActivity) = flow {
|
||||||
emitAll(biometricHelper.authenticate(activity))
|
emitAll(biometricHelper.authenticate(activity))
|
||||||
}.catch { error ->
|
}.catch { error ->
|
||||||
when {
|
when {
|
||||||
versionProvider.get() >= Build.VERSION_CODES.M && error is KeyPermanentlyInvalidatedException -> {
|
versionProvider.isAtLeast(Build.VERSION_CODES.M) &&
|
||||||
|
error is KeyPermanentlyInvalidatedException -> {
|
||||||
onBiometricKeyInvalidated()
|
onBiometricKeyInvalidated()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@ -168,7 +167,6 @@ class LockScreenViewModel @AssistedInject constructor(
|
|||||||
_viewEvents.post(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage)
|
_viewEvents.post(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private suspend fun updateStateWithBiometricInfo() {
|
private suspend fun updateStateWithBiometricInfo() {
|
||||||
// This is a terrible hack, but I found no other way to ensure this would be called only after the device is considered unlocked on Android 12+
|
// This is a terrible hack, but I found no other way to ensure this would be called only after the device is considered unlocked on Android 12+
|
||||||
waitUntilKeyguardIsUnlocked()
|
waitUntilKeyguardIsUnlocked()
|
||||||
@ -191,9 +189,8 @@ class LockScreenViewModel @AssistedInject constructor(
|
|||||||
* after an Activity's `onResume` method. If we mix that with the system keys needing the device to be unlocked before they're used, we get crashes.
|
* after an Activity's `onResume` method. If we mix that with the system keys needing the device to be unlocked before they're used, we get crashes.
|
||||||
* See issue [#6768](https://github.com/vector-im/element-android/issues/6768).
|
* See issue [#6768](https://github.com/vector-im/element-android/issues/6768).
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private suspend fun waitUntilKeyguardIsUnlocked() {
|
private suspend fun waitUntilKeyguardIsUnlocked() {
|
||||||
if (versionProvider.get() < Build.VERSION_CODES.S) return
|
if (versionProvider.isAtLeast(Build.VERSION_CODES.S)) {
|
||||||
withTimeoutOrNull(5.seconds) {
|
withTimeoutOrNull(5.seconds) {
|
||||||
while (keyguardManager.isDeviceLocked) {
|
while (keyguardManager.isDeviceLocked) {
|
||||||
delay(50.milliseconds)
|
delay(50.milliseconds)
|
||||||
@ -201,3 +198,4 @@ class LockScreenViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -50,10 +50,10 @@ class FallbackBiometricDialogFragment : DialogFragment(R.layout.fragment_biometr
|
|||||||
|
|
||||||
private val parsedArgs by args<Args>()
|
private val parsedArgs by args<Args>()
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
retainInstance = true
|
retainInstance = true
|
||||||
|
|
||||||
setStyle(STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog)
|
setStyle(STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog)
|
||||||
|
@ -31,7 +31,7 @@ import im.vector.app.core.di.ActiveSessionHolder
|
|||||||
import im.vector.app.core.extensions.getAllChildFragments
|
import im.vector.app.core.extensions.getAllChildFragments
|
||||||
import im.vector.app.core.extensions.toOnOff
|
import im.vector.app.core.extensions.toOnOff
|
||||||
import im.vector.app.core.resources.BuildMeta
|
import im.vector.app.core.resources.BuildMeta
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocaleProvider
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.settings.devtools.GossipingEventsSerializer
|
import im.vector.app.features.settings.devtools.GossipingEventsSerializer
|
||||||
import im.vector.app.features.settings.locale.SystemLocaleProvider
|
import im.vector.app.features.settings.locale.SystemLocaleProvider
|
||||||
@ -80,6 +80,7 @@ class BugReporter @Inject constructor(
|
|||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val processInfo: ProcessInfo,
|
private val processInfo: ProcessInfo,
|
||||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
|
private val vectorLocale: VectorLocaleProvider,
|
||||||
) {
|
) {
|
||||||
var inMultiWindowMode = false
|
var inMultiWindowMode = false
|
||||||
|
|
||||||
@ -294,7 +295,7 @@ class BugReporter @Inject constructor(
|
|||||||
Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME
|
Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME
|
||||||
)
|
)
|
||||||
.addFormDataPart("locale", Locale.getDefault().toString())
|
.addFormDataPart("locale", Locale.getDefault().toString())
|
||||||
.addFormDataPart("app_language", VectorLocale.applicationLocale.toString())
|
.addFormDataPart("app_language", vectorLocale.applicationLocale.toString())
|
||||||
.addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
|
.addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
|
||||||
.addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
|
.addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
|
||||||
.addFormDataPart("server_version", serverVersion)
|
.addFormDataPart("server_version", serverVersion)
|
||||||
|
@ -16,10 +16,10 @@
|
|||||||
|
|
||||||
package im.vector.app.features.rageshake
|
package im.vector.app.features.rageshake
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
import im.vector.app.core.resources.VersionCodeProvider
|
import im.vector.app.core.resources.VersionCodeProvider
|
||||||
import im.vector.app.features.version.VersionProvider
|
import im.vector.app.features.version.VersionProvider
|
||||||
import org.matrix.android.sdk.api.Matrix
|
import org.matrix.android.sdk.api.Matrix
|
||||||
@ -31,10 +31,11 @@ import javax.inject.Singleton
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class VectorUncaughtExceptionHandler @Inject constructor(
|
class VectorUncaughtExceptionHandler @Inject constructor(
|
||||||
context: Context,
|
@DefaultPreferences
|
||||||
|
private val preferences: SharedPreferences,
|
||||||
private val bugReporter: BugReporter,
|
private val bugReporter: BugReporter,
|
||||||
private val versionProvider: VersionProvider,
|
private val versionProvider: VersionProvider,
|
||||||
private val versionCodeProvider: VersionCodeProvider
|
private val versionCodeProvider: VersionCodeProvider,
|
||||||
) : Thread.UncaughtExceptionHandler {
|
) : Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
// key to save the crash status
|
// key to save the crash status
|
||||||
@ -44,8 +45,6 @@ class VectorUncaughtExceptionHandler @Inject constructor(
|
|||||||
|
|
||||||
private var previousHandler: Thread.UncaughtExceptionHandler? = null
|
private var previousHandler: Thread.UncaughtExceptionHandler? = null
|
||||||
|
|
||||||
private val preferences = DefaultSharedPreferences.getInstance(context)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activate this handler.
|
* Activate this handler.
|
||||||
*/
|
*/
|
||||||
|
@ -82,15 +82,18 @@ class RoomUploadsMediaFragment :
|
|||||||
controller.listener = this
|
controller.listener = this
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun getNumberOfColumns(): Int {
|
private fun getNumberOfColumns(): Int {
|
||||||
val displayMetrics = DisplayMetrics()
|
val screenWidthInPx = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val a = requireActivity().windowManager.currentWindowMetrics
|
||||||
requireContext().display?.getMetrics(displayMetrics)
|
a.bounds.width()
|
||||||
} else {
|
} else {
|
||||||
|
val displayMetrics = DisplayMetrics()
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
|
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||||
|
displayMetrics.widthPixels
|
||||||
}
|
}
|
||||||
return dimensionConverter.pxToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP
|
val screenWidthInDp = dimensionConverter.pxToDp(screenWidthInPx)
|
||||||
|
return screenWidthInDp / IMAGE_SIZE_DP
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
@ -17,30 +17,40 @@
|
|||||||
package im.vector.app.features.settings
|
package im.vector.app.features.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
import im.vector.app.core.resources.BuildMeta
|
import im.vector.app.core.resources.BuildMeta
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.IllformedLocaleException
|
import java.util.IllformedLocaleException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object to manage the Locale choice of the user.
|
* Object to manage the Locale choice of the user.
|
||||||
*/
|
*/
|
||||||
object VectorLocale {
|
@Singleton
|
||||||
private const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY"
|
class VectorLocale @Inject constructor(
|
||||||
private const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY"
|
private val context: Context,
|
||||||
private const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY"
|
private val buildMeta: BuildMeta,
|
||||||
|
@DefaultPreferences
|
||||||
|
private val preferences: SharedPreferences,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY"
|
||||||
|
const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY"
|
||||||
|
const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY"
|
||||||
private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY"
|
private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY"
|
||||||
|
private const val ISO_15924_LATN = "Latn"
|
||||||
|
}
|
||||||
|
|
||||||
private val defaultLocale = Locale("en", "US")
|
private val defaultLocale = Locale("en", "US")
|
||||||
|
|
||||||
private const val ISO_15924_LATN = "Latn"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The cache of supported application languages.
|
* The cache of supported application languages.
|
||||||
*/
|
*/
|
||||||
@ -52,17 +62,10 @@ object VectorLocale {
|
|||||||
var applicationLocale = defaultLocale
|
var applicationLocale = defaultLocale
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private lateinit var context: Context
|
|
||||||
private lateinit var buildMeta: BuildMeta
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init this object.
|
* Init this singleton.
|
||||||
*/
|
*/
|
||||||
fun init(context: Context, buildMeta: BuildMeta) {
|
fun init() {
|
||||||
this.context = context
|
|
||||||
this.buildMeta = buildMeta
|
|
||||||
val preferences = DefaultSharedPreferences.getInstance(context)
|
|
||||||
|
|
||||||
if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) {
|
if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) {
|
||||||
applicationLocale = Locale(
|
applicationLocale = Locale(
|
||||||
preferences.getString(APPLICATION_LOCALE_LANGUAGE_KEY, "")!!,
|
preferences.getString(APPLICATION_LOCALE_LANGUAGE_KEY, "")!!,
|
||||||
@ -88,7 +91,7 @@ object VectorLocale {
|
|||||||
fun saveApplicationLocale(locale: Locale) {
|
fun saveApplicationLocale(locale: Locale) {
|
||||||
applicationLocale = locale
|
applicationLocale = locale
|
||||||
|
|
||||||
DefaultSharedPreferences.getInstance(context).edit {
|
preferences.edit {
|
||||||
val language = locale.language
|
val language = locale.language
|
||||||
if (language.isEmpty()) {
|
if (language.isEmpty()) {
|
||||||
remove(APPLICATION_LOCALE_LANGUAGE_KEY)
|
remove(APPLICATION_LOCALE_LANGUAGE_KEY)
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.settings
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to provide the Locale choice of the user.
|
||||||
|
*/
|
||||||
|
class VectorLocaleProvider @Inject constructor(
|
||||||
|
@DefaultPreferences
|
||||||
|
private val preferences: SharedPreferences,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Get the current local.
|
||||||
|
* SharedPref values has been initialized in [VectorLocale.init]
|
||||||
|
*/
|
||||||
|
val applicationLocale: Locale
|
||||||
|
get() = Locale(
|
||||||
|
preferences.getString(VectorLocale.APPLICATION_LOCALE_LANGUAGE_KEY, "")!!,
|
||||||
|
preferences.getString(VectorLocale.APPLICATION_LOCALE_COUNTRY_KEY, "")!!,
|
||||||
|
preferences.getString(VectorLocale.APPLICATION_LOCALE_VARIANT_KEY, "")!!
|
||||||
|
)
|
||||||
|
}
|
@ -24,8 +24,9 @@ import androidx.annotation.BoolRes
|
|||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.squareup.seismic.ShakeDetector
|
import com.squareup.seismic.ShakeDetector
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.DefaultSharedPreferences
|
import im.vector.app.core.di.DefaultPreferences
|
||||||
import im.vector.app.core.resources.BuildMeta
|
import im.vector.app.core.resources.BuildMeta
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.time.Clock
|
import im.vector.app.core.time.Clock
|
||||||
import im.vector.app.features.VectorFeatures
|
import im.vector.app.features.VectorFeatures
|
||||||
import im.vector.app.features.disclaimer.SHARED_PREF_KEY
|
import im.vector.app.features.disclaimer.SHARED_PREF_KEY
|
||||||
@ -41,6 +42,9 @@ class VectorPreferences @Inject constructor(
|
|||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val vectorFeatures: VectorFeatures,
|
private val vectorFeatures: VectorFeatures,
|
||||||
|
@DefaultPreferences
|
||||||
|
private val defaultPrefs: SharedPreferences,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -289,8 +293,6 @@ class VectorPreferences @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defaultPrefs = DefaultSharedPreferences.getInstance(context)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow subscribing and unsubscribing to configuration changes. This is
|
* Allow subscribing and unsubscribing to configuration changes. This is
|
||||||
* particularly useful when you need to be notified of a configuration change
|
* particularly useful when you need to be notified of a configuration change
|
||||||
@ -716,10 +718,10 @@ class VectorPreferences @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
fun getSelectedMediasSavingPeriodString(): String {
|
fun getSelectedMediasSavingPeriodString(): String {
|
||||||
return when (getSelectedMediasSavingPeriod()) {
|
return when (getSelectedMediasSavingPeriod()) {
|
||||||
MEDIA_SAVING_3_DAYS -> context.getString(R.string.media_saving_period_3_days)
|
MEDIA_SAVING_3_DAYS -> stringProvider.getString(R.string.media_saving_period_3_days)
|
||||||
MEDIA_SAVING_1_WEEK -> context.getString(R.string.media_saving_period_1_week)
|
MEDIA_SAVING_1_WEEK -> stringProvider.getString(R.string.media_saving_period_1_week)
|
||||||
MEDIA_SAVING_1_MONTH -> context.getString(R.string.media_saving_period_1_month)
|
MEDIA_SAVING_1_MONTH -> stringProvider.getString(R.string.media_saving_period_1_month)
|
||||||
MEDIA_SAVING_FOREVER -> context.getString(R.string.media_saving_period_forever)
|
MEDIA_SAVING_FOREVER -> stringProvider.getString(R.string.media_saving_period_forever)
|
||||||
else -> "?"
|
else -> "?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ class VectorSettingsPreferencesFragment :
|
|||||||
@Inject lateinit var vectorPreferences: VectorPreferences
|
@Inject lateinit var vectorPreferences: VectorPreferences
|
||||||
@Inject lateinit var fontScalePreferences: FontScalePreferences
|
@Inject lateinit var fontScalePreferences: FontScalePreferences
|
||||||
@Inject lateinit var vectorFeatures: VectorFeatures
|
@Inject lateinit var vectorFeatures: VectorFeatures
|
||||||
|
@Inject lateinit var vectorLocale: VectorLocale
|
||||||
|
|
||||||
override var titleRes = R.string.settings_preferences
|
override var titleRes = R.string.settings_preferences
|
||||||
override val preferenceXmlRes = R.xml.vector_settings_preferences
|
override val preferenceXmlRes = R.xml.vector_settings_preferences
|
||||||
@ -198,7 +199,7 @@ class VectorSettingsPreferencesFragment :
|
|||||||
|
|
||||||
private fun setUserInterfacePreferences() {
|
private fun setUserInterfacePreferences() {
|
||||||
// Selected language
|
// Selected language
|
||||||
selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale)
|
selectedLanguagePreference.summary = vectorLocale.localeToLocalisedString(vectorLocale.applicationLocale)
|
||||||
|
|
||||||
// Text size
|
// Text size
|
||||||
textSizePreference.summary = getString(fontScalePreferences.getResolvedFontScaleValue().nameResId)
|
textSizePreference.summary = getString(fontScalePreferences.getResolvedFontScaleValue().nameResId)
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.settings
|
package im.vector.app.features.settings
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -448,7 +447,6 @@ class VectorSettingsSecurityPrivacyFragment :
|
|||||||
/**
|
/**
|
||||||
* Manage the e2e keys import.
|
* Manage the e2e keys import.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private fun importKeys() {
|
private fun importKeys() {
|
||||||
openFileSelection(
|
openFileSelection(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
|
@ -23,17 +23,19 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.SwitchPreference
|
import androidx.preference.SwitchPreference
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.preference.VectorPreference
|
import im.vector.app.core.preference.VectorPreference
|
||||||
import im.vector.app.core.utils.getCallRingtoneName
|
import im.vector.app.core.utils.RingtoneUtils
|
||||||
import im.vector.app.core.utils.getCallRingtoneUri
|
|
||||||
import im.vector.app.core.utils.setCallRingtoneUri
|
|
||||||
import im.vector.app.core.utils.setUseRiotDefaultRingtone
|
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() {
|
class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() {
|
||||||
|
|
||||||
|
@Inject lateinit var ringtoneUtils: RingtoneUtils
|
||||||
|
|
||||||
override var titleRes = R.string.preference_voice_and_video
|
override var titleRes = R.string.preference_voice_and_video
|
||||||
override val preferenceXmlRes = R.xml.vector_settings_voice_video
|
override val preferenceXmlRes = R.xml.vector_settings_voice_video
|
||||||
|
|
||||||
@ -52,12 +54,12 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() {
|
|||||||
override fun bindPref() {
|
override fun bindPref() {
|
||||||
// Incoming call sounds
|
// Incoming call sounds
|
||||||
mUseRiotCallRingtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
mUseRiotCallRingtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
activity?.let { setUseRiotDefaultRingtone(it, mUseRiotCallRingtonePreference.isChecked) }
|
ringtoneUtils.setUseRiotDefaultRingtone(mUseRiotCallRingtonePreference.isChecked)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
mCallRingtonePreference.let {
|
mCallRingtonePreference.let {
|
||||||
activity?.let { activity -> it.summary = getCallRingtoneName(activity) }
|
it.summary = ringtoneUtils.getCallRingtoneName()
|
||||||
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
displayRingtonePicker()
|
displayRingtonePicker()
|
||||||
false
|
false
|
||||||
@ -68,10 +70,9 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() {
|
|||||||
private val ringtoneStartForActivityResult = registerStartForActivityResult { activityResult ->
|
private val ringtoneStartForActivityResult = registerStartForActivityResult { activityResult ->
|
||||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
val callRingtoneUri: Uri? = activityResult.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
val callRingtoneUri: Uri? = activityResult.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||||
val thisActivity = activity
|
if (callRingtoneUri != null) {
|
||||||
if (callRingtoneUri != null && thisActivity != null) {
|
ringtoneUtils.setCallRingtoneUri(callRingtoneUri)
|
||||||
setCallRingtoneUri(thisActivity, callRingtoneUri)
|
mCallRingtonePreference.summary = ringtoneUtils.getCallRingtoneName()
|
||||||
mCallRingtonePreference.summary = getCallRingtoneName(thisActivity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,7 +83,7 @@ class VectorSettingsVoiceVideoFragment : VectorSettingsBaseFragment() {
|
|||||||
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
|
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
|
||||||
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
|
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
|
||||||
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE)
|
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE)
|
||||||
activity?.let { putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, getCallRingtoneUri(it)) }
|
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUtils.getCallRingtoneUri())
|
||||||
}
|
}
|
||||||
ringtoneStartForActivityResult.launch(intent)
|
ringtoneStartForActivityResult.launch(intent)
|
||||||
}
|
}
|
||||||
|
@ -37,13 +37,15 @@ import javax.inject.Inject
|
|||||||
class LocalePickerController @Inject constructor(
|
class LocalePickerController @Inject constructor(
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val errorFormatter: ErrorFormatter
|
private val errorFormatter: ErrorFormatter,
|
||||||
|
private val vectorLocale: VectorLocale,
|
||||||
) : TypedEpoxyController<LocalePickerViewState>() {
|
) : TypedEpoxyController<LocalePickerViewState>() {
|
||||||
|
|
||||||
var listener: Listener? = null
|
var listener: Listener? = null
|
||||||
|
|
||||||
override fun buildModels(data: LocalePickerViewState?) {
|
override fun buildModels(data: LocalePickerViewState?) {
|
||||||
val list = data?.locales ?: return
|
val list = data?.locales ?: return
|
||||||
|
val currentLocale = data.currentLocale ?: return
|
||||||
val host = this
|
val host = this
|
||||||
|
|
||||||
profileSectionItem {
|
profileSectionItem {
|
||||||
@ -51,10 +53,10 @@ class LocalePickerController @Inject constructor(
|
|||||||
title(host.stringProvider.getString(R.string.choose_locale_current_locale_title))
|
title(host.stringProvider.getString(R.string.choose_locale_current_locale_title))
|
||||||
}
|
}
|
||||||
localeItem {
|
localeItem {
|
||||||
id(data.currentLocale.toString())
|
id(currentLocale.toString())
|
||||||
title(VectorLocale.localeToLocalisedString(data.currentLocale).safeCapitalize(data.currentLocale))
|
title(host.vectorLocale.localeToLocalisedString(currentLocale).safeCapitalize(currentLocale))
|
||||||
if (host.vectorPreferences.developerMode()) {
|
if (host.vectorPreferences.developerMode()) {
|
||||||
subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale))
|
subtitle(host.vectorLocale.localeToLocalisedStringInfo(currentLocale))
|
||||||
}
|
}
|
||||||
clickListener { host.listener?.onUseCurrentClicked() }
|
clickListener { host.listener?.onUseCurrentClicked() }
|
||||||
}
|
}
|
||||||
@ -78,13 +80,13 @@ class LocalePickerController @Inject constructor(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
list()
|
list()
|
||||||
.filter { it.toString() != data.currentLocale.toString() }
|
.filter { it.toString() != currentLocale.toString() }
|
||||||
.forEach { locale ->
|
.forEach { locale ->
|
||||||
localeItem {
|
localeItem {
|
||||||
id(locale.toString())
|
id(locale.toString())
|
||||||
title(VectorLocale.localeToLocalisedString(locale).safeCapitalize(locale))
|
title(host.vectorLocale.localeToLocalisedString(locale).safeCapitalize(locale))
|
||||||
if (host.vectorPreferences.developerMode()) {
|
if (host.vectorPreferences.developerMode()) {
|
||||||
subtitle(VectorLocale.localeToLocalisedStringInfo(locale))
|
subtitle(host.vectorLocale.localeToLocalisedStringInfo(locale))
|
||||||
}
|
}
|
||||||
clickListener { host.listener?.onLocaleClicked(locale) }
|
clickListener { host.listener?.onLocaleClicked(locale) }
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,8 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
class LocalePickerViewModel @AssistedInject constructor(
|
class LocalePickerViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: LocalePickerViewState,
|
@Assisted initialState: LocalePickerViewState,
|
||||||
private val vectorConfiguration: VectorConfiguration
|
private val vectorConfiguration: VectorConfiguration,
|
||||||
|
private val vectorLocale: VectorLocale,
|
||||||
) : VectorViewModel<LocalePickerViewState, LocalePickerAction, LocalePickerViewEvents>(initialState) {
|
) : VectorViewModel<LocalePickerViewState, LocalePickerAction, LocalePickerViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ -39,8 +40,13 @@ class LocalePickerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
currentLocale = vectorLocale.applicationLocale
|
||||||
|
)
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = VectorLocale.getSupportedLocales()
|
val result = vectorLocale.getSupportedLocales()
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
@ -59,7 +65,7 @@ class LocalePickerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) {
|
private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) {
|
||||||
VectorLocale.saveApplicationLocale(action.locale)
|
vectorLocale.saveApplicationLocale(action.locale)
|
||||||
vectorConfiguration.applyToApplicationContext()
|
vectorConfiguration.applyToApplicationContext()
|
||||||
_viewEvents.post(LocalePickerViewEvents.RestartActivity)
|
_viewEvents.post(LocalePickerViewEvents.RestartActivity)
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,9 @@ package im.vector.app.features.settings.locale
|
|||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.app.features.settings.VectorLocale
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
data class LocalePickerViewState(
|
data class LocalePickerViewState(
|
||||||
val currentLocale: Locale = VectorLocale.applicationLocale,
|
val currentLocale: Locale? = null,
|
||||||
val locales: Async<List<Locale>> = Uninitialized
|
val locales: Async<List<Locale>> = Uninitialized
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user