This commit is contained in:
Benoit Marty
2019-05-16 10:23:57 +02:00
committed by Benoit Marty
parent 6aae943e77
commit 1436667e7d
303 changed files with 25671 additions and 506 deletions

View File

@ -174,6 +174,12 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"
// Passphrase strength helper
implementation 'com.nulab-inc:zxcvbn:1.2.5'
//Alerter
implementation 'com.tapadoo.android:alerter:3.0.2'
implementation 'com.otaliastudios:autocomplete:1.1.0'
// Butterknife

View File

@ -27,7 +27,7 @@ class TestTokenRegistration(val fragment: Fragment) : TroubleshootTest(R.string.
override fun perform() {
/*
TODO
Matrix.getInstance(VectorApp.getInstance().baseContext).pushManager.forceSessionsRegistration(object : ApiCallback<Void> {
Matrix.getInstance(VectorApp.getInstance().baseContext).pushManager.forceSessionsRegistration(object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_success)
status = TestStatus.SUCCESS

View File

@ -38,6 +38,18 @@
android:label="@string/title_activity_settings"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".features.media.VideoMediaViewerActivity" />
<activity
android:name=".features.crypto.verification.SASVerificationActivity"
android:label="@string/title_activity_verify_device" />
<activity
android:name="im.vector.riotredesign.features.crypto.keysbackup.restore.KeysBackupRestoreActivity"
android:label="@string/title_activity_keys_backup_setup" />
<activity
android:name="im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity"
android:label="@string/title_activity_keys_backup_restore" />
<activity
android:name="im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity"
android:label="@string/encryption_message_recovery" />
<activity
android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity"

View File

@ -28,8 +28,10 @@ import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.di.AppModule
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotredesign.features.rageshake.VectorFileLogger
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import org.koin.android.logger.AndroidLogger
import org.koin.log.EmptyLogger
import org.koin.standalone.StandAloneContext.startKoin
import timber.log.Timber
@ -37,9 +39,13 @@ import timber.log.Timber
class VectorApplication : Application() {
lateinit var appContext: Context
override fun onCreate() {
super.onCreate()
appContext = this
VectorUncaughtExceptionHandler.activate(this)
// Log
@ -56,9 +62,13 @@ class VectorApplication : Application() {
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
val appModule = AppModule(applicationContext).definition
val homeModule = HomeModule().definition
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
startKoin(
list = listOf(appModule, homeModule),
logger = if (BuildConfig.DEBUG) AndroidLogger() else EmptyLogger())
Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION)
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())
}
override fun attachBaseContext(base: Context) {

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
@ -73,6 +74,10 @@ class AppModule(private val context: Context) {
Matrix.getInstance().currentSession!!
}
single {
IncomingVerificationRequestHandler(get(), get(), get())
}
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.platform
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import butterknife.BindView
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.hideKeyboard
import org.koin.android.ext.android.get
/**
* Simple activity with a toolbar, a waiting overlay, and a fragment container and a mxSession.
*/
abstract class SimpleFragmentActivity : VectorBaseActivity() {
override fun getLayoutRes() = R.layout.activity
@BindView(R.id.waiting_view_status_circular_progress)
lateinit var waitingCircularProgress: View
@BindView(R.id.waiting_view_status_text)
lateinit var waitingStatusText: TextView
@BindView(R.id.waiting_view_status_horizontal_progress)
lateinit var waitingHorizontalProgress: ProgressBar
protected val mSession = get<Session>()
override fun initUiAndData() {
configureToolbar()
waitingView = findViewById(R.id.waiting_view)
}
/**
* Displays a progress indicator with a message to the user.
* Blocks user interactions.
*/
fun updateWaitingView(data: WaitingViewData?) {
data?.let {
waitingStatusText.text = data.message
if (data.progress != null && data.progressTotal != null) {
waitingHorizontalProgress.isIndeterminate = false
waitingHorizontalProgress.progress = data.progress
waitingHorizontalProgress.max = data.progressTotal
waitingHorizontalProgress.isVisible = true
waitingCircularProgress.isVisible = false
} else if (data.isIndeterminate) {
waitingHorizontalProgress.isIndeterminate = true
waitingHorizontalProgress.isVisible = true
waitingCircularProgress.isVisible = false
} else {
waitingHorizontalProgress.isVisible = false
waitingCircularProgress.isVisible = true
}
showWaitingView()
} ?: run {
hideWaitingView()
}
}
override fun showWaitingView() {
hideKeyboard()
waitingStatusText.isGone = waitingStatusText.text.isNullOrBlank()
super.showWaitingView()
}
override fun hideWaitingView() {
waitingStatusText.text = null
waitingStatusText.isGone = true
waitingHorizontalProgress.progress = 0
waitingHorizontalProgress.isVisible = false
super.hideWaitingView()
}
override fun onBackPressed() {
if (waitingView!!.isVisible) {
// ignore
return
}
super.onBackPressed()
}
}

View File

@ -24,6 +24,7 @@ import android.view.View
import androidx.annotation.*
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.Unbinder
@ -254,6 +255,39 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
}
}
//==============================================================================================
// Handle loading view (also called waiting view or spinner view)
//==============================================================================================
var waitingView: View? = null
set(value) {
field = value
// Ensure this view is clickable to catch UI events
value?.isClickable = true
}
/**
* Tells if the waiting view is currently displayed
*
* @return true if the waiting view is displayed
*/
fun isWaitingViewVisible() = waitingView?.isVisible == true
/**
* Show the waiting view
*/
open fun showWaitingView() {
waitingView?.isVisible = true
}
/**
* Hide the waiting view
*/
open fun hideWaitingView() {
waitingView?.isVisible = false
}
/* ==========================================================================================
* OPEN METHODS
* ========================================================================================== */

View File

@ -0,0 +1,27 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.platform
/**
* Model to display a Waiting View
*/
data class WaitingViewData(
val message: String,
val progress: Int? = null,
val progressTotal: Int? = null,
val isIndeterminate: Boolean = false
)

View File

@ -133,7 +133,7 @@ open class VectorPreference : Preference {
}
} catch (e: Exception) {
Timber.e(LOG_TAG, "onBindView " + e.message, e)
Timber.e("onBindView " + e.message, e)
}
super.onBindViewHolder(holder)

View File

@ -0,0 +1,98 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.resources
import android.content.Context
import android.net.Uri
import android.text.TextUtils
import android.webkit.MimeTypeMap
import im.vector.riotredesign.core.utils.getFileExtension
import timber.log.Timber
import java.io.InputStream
/**
* Mime types
*/
const val MIME_TYPE_JPEG = "image/jpeg"
const val MIME_TYPE_JPG = "image/jpg"
const val MIME_TYPE_IMAGE_ALL = "image/*"
const val MIME_TYPE_ALL_CONTENT = "*/*"
data class Resource(
var mContentStream: InputStream? = null,
var mMimeType: String? = null
) {
/**
* Close the content stream.
*/
fun close() {
try {
mMimeType = null
mContentStream?.close()
mContentStream = null
} catch (e: Exception) {
Timber.e(e, "Resource.close failed")
}
}
/**
* Tells if the opened resource is a jpeg one.
*
* @return true if the opened resource is a jpeg one.
*/
fun isJpegResource(): Boolean {
return MIME_TYPE_JPEG == mMimeType || MIME_TYPE_JPG == mMimeType
}
}
/**
* Get a resource stream and metadata about it given its URI returned from onActivityResult.
*
* @param context the context.
* @param uri the URI
* @param mimetype the mimetype
* @return a [Resource] encapsulating the opened resource stream and associated metadata
* or `null` if opening the resource stream failed.
*/
fun openResource(context: Context, uri: Uri, mimetype: String?): Resource? {
var mimetype = mimetype
try {
// if the mime type is not provided, try to find it out
if (TextUtils.isEmpty(mimetype)) {
mimetype = context.contentResolver.getType(uri)
// try to find the mimetype from the filename
if (null == mimetype) {
val extension = getFileExtension(uri.toString())
if (extension != null) {
mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
}
}
}
return Resource(
context.contentResolver.openInputStream(uri),
mimetype)
} catch (e: Exception) {
Timber.e(e, "Failed to open resource input stream")
}
return null
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.ui.list
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.LayoutRes
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotredesign.R
/**
* View Holder for generic list items.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
class GenericItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
companion object {
@LayoutRes
const val resId = R.layout.item_generic_list
}
@BindView(R.id.item_generic_title_text)
lateinit var titleText: TextView
@BindView(R.id.item_generic_description_text)
lateinit var descriptionText: TextView
@BindView(R.id.item_generic_accessory_image)
lateinit var accessoryImage: ImageView
@BindView(R.id.item_generic_progress_bar)
lateinit var progressBar: ProgressBar
@BindView(R.id.item_generic_action_button)
lateinit var actionButton: Button
init {
ButterKnife.bind(this, itemView)
}
fun bind(item: GenericRecyclerViewItem) {
titleText.text = item.title
when (item.style) {
GenericRecyclerViewItem.STYLE.BIG_TEXT -> titleText.textSize = 18f
GenericRecyclerViewItem.STYLE.NORMAL_TEXT -> titleText.textSize = 14f
}
item.description?.let {
descriptionText.isVisible = true
descriptionText.text = it
} ?: run { descriptionText.isVisible = false }
if (item.hasIndeterminateProcess) {
progressBar.isVisible = true
accessoryImage.isVisible = false
} else {
progressBar.isVisible = false
if (item.endIconResourceId != -1) {
accessoryImage.setImageResource(item.endIconResourceId)
accessoryImage.isVisible = true
} else {
accessoryImage.isVisible = false
}
}
val buttonAction = item.buttonAction
if (buttonAction == null) {
actionButton.isVisible = false
} else {
actionButton.text = buttonAction.title
actionButton.setOnClickListener {
buttonAction.perform?.run()
}
actionButton.isVisible = true
}
itemView?.setOnClickListener {
item.itemClickAction?.perform?.run()
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.ui.list
import androidx.annotation.DrawableRes
/**
* A generic list item.
* Displays an item with a title, and optional description.
* Can display an accessory on the right, that can be an image or an indeterminate progress.
* If provided with an action, will display a button at the bottom of the list item.
*/
class GenericRecyclerViewItem(val title: String,
var description: String? = null,
val style: STYLE = STYLE.NORMAL_TEXT) {
enum class STYLE {
BIG_TEXT,
NORMAL_TEXT
}
@DrawableRes
var endIconResourceId: Int = -1
var hasIndeterminateProcess = false
var buttonAction: Action? = null
var itemClickAction: Action? = null
class Action(var title: String) {
var perform: Runnable? = null
}
}

View File

@ -0,0 +1,293 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.ui.views
import android.content.Context
import android.preference.PreferenceManager
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.OnClick
import im.vector.riotredesign.R
import timber.log.Timber
/**
* The view used in VectorHomeActivity to show some information about the keys backup state
* It does have a unique render method
*/
class KeysBackupBanner @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
@BindView(R.id.view_keys_backup_banner_text_1)
lateinit var textView1: TextView
@BindView(R.id.view_keys_backup_banner_text_2)
lateinit var textView2: TextView
@BindView(R.id.view_keys_backup_banner_close_group)
lateinit var close: View
@BindView(R.id.view_keys_backup_banner_loading)
lateinit var loading: View
var delegate: Delegate? = null
private var state: State = State.Initial
private var scrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE
set(value) {
field = value
val pendingV = pendingVisibility
if (pendingV != null) {
pendingVisibility = null
visibility = pendingV
}
}
private var pendingVisibility: Int? = null
init {
setupView()
}
/**
* This methods is responsible for rendering the view according to the newState
*
* @param newState the newState representing the view
*/
fun render(newState: State, force: Boolean = false) {
if (newState == state && !force) {
Timber.d("State unchanged")
return
}
Timber.d("Rendering $newState")
state = newState
hideAll()
when (newState) {
State.Initial -> renderInitial()
State.Hidden -> renderHidden()
is State.Setup -> renderSetup(newState.numberOfKeys)
is State.Recover -> renderRecover(newState.version)
is State.Update -> renderUpdate(newState.version)
State.BackingUp -> renderBackingUp()
}
}
override fun setVisibility(visibility: Int) {
if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
// Wait for scroll state to be idle
pendingVisibility = visibility
return
}
if (visibility != getVisibility()) {
// Schedule animation
val parent = parent as ViewGroup
TransitionManager.beginDelayedTransition(parent)
}
super.setVisibility(visibility)
}
override fun onClick(v: View?) {
when (state) {
is State.Setup -> {
delegate?.setupKeysBackup()
}
is State.Recover -> {
delegate?.recoverKeysBackup()
}
}
}
@OnClick(R.id.view_keys_backup_banner_close)
internal fun onCloseClicked() {
state.let {
when (it) {
is State.Setup -> {
PreferenceManager.getDefaultSharedPreferences(context).edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true)
}
}
is State.Recover -> {
PreferenceManager.getDefaultSharedPreferences(context).edit {
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version)
}
}
is State.Update -> {
PreferenceManager.getDefaultSharedPreferences(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
render(state, true)
}
// PRIVATE METHODS *****************************************************************************************************************************************
private fun setupView() {
inflate(context, R.layout.view_keys_backup_banner, this)
ButterKnife.bind(this)
setOnClickListener(this)
}
private fun renderInitial() {
isVisible = false
}
private fun renderHidden() {
isVisible = false
}
private fun renderSetup(nbOfKeys: Int) {
if (nbOfKeys == 0
|| PreferenceManager.getDefaultSharedPreferences(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
isVisible = false
} else {
isVisible = true
textView1.setText(R.string.keys_backup_banner_setup_line1)
textView2.isVisible = true
textView2.setText(R.string.keys_backup_banner_setup_line2)
close.isVisible = true
}
}
private fun renderRecover(version: String) {
if (version == PreferenceManager.getDefaultSharedPreferences(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) {
isVisible = false
} else {
isVisible = true
textView1.setText(R.string.keys_backup_banner_recover_line1)
textView2.isVisible = true
textView2.setText(R.string.keys_backup_banner_recover_line2)
close.isVisible = true
}
}
private fun renderUpdate(version: String) {
if (version == PreferenceManager.getDefaultSharedPreferences(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) {
isVisible = false
} else {
isVisible = true
textView1.setText(R.string.keys_backup_banner_update_line1)
textView2.isVisible = true
textView2.setText(R.string.keys_backup_banner_update_line2)
close.isVisible = true
}
}
private fun renderBackingUp() {
isVisible = true
textView1.setText(R.string.keys_backup_banner_in_progress)
loading.isVisible = true
}
/**
* Hide all views that are not visible in all state
*/
private fun hideAll() {
textView2.isVisible = false
close.isVisible = false
loading.isVisible = false
}
/**
* The state representing the view
* It can take one state at a time
*/
sealed class State {
// Not yet rendered
object Initial : State()
// View will be Gone
object Hidden : State()
// Keys backup is not setup, numberOfKeys is the number of locally stored keys
data class Setup(val numberOfKeys: Int) : State()
// Keys backup can be recovered, with version from the server
data class Recover(val version: String) : State()
// Keys backup can be updated
data class Update(val version: String) : State()
// Keys are backing up
object BackingUp : State()
}
/**
* An interface to delegate some actions to another object
*/
interface Delegate {
fun setupKeysBackup()
fun recoverKeysBackup()
}
companion object {
/**
* Preference key for setup. Value is a boolean.
*/
private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN"
/**
* Preference key for recover. Value is a backup version (String).
*/
private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION"
/**
* Preference key for update. Value is a backup version (String).
*/
private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION"
/**
* Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version
*/
fun onRecoverDoneForVersion(context: Context, version: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit {
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version)
}
}
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.annotation.IntRange
import butterknife.BindColor
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotredesign.R
/**
* A password strength bar custom widget
* Strength is an Integer
* -> 0 No strength
* -> 1 Weak
* -> 2 Fair
* -> 3 Good
* -> 4 Strong
*/
class PasswordStrengthBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: LinearLayout(context, attrs, defStyleAttr) {
@BindView(R.id.password_strength_bar_1)
lateinit var bar1: View
@BindView(R.id.password_strength_bar_2)
lateinit var bar2: View
@BindView(R.id.password_strength_bar_3)
lateinit var bar3: View
@BindView(R.id.password_strength_bar_4)
lateinit var bar4: View
@BindColor(R.color.password_strength_bar_undefined)
@JvmField
var colorBackground: Int = 0
@BindColor(R.color.password_strength_bar_weak)
@JvmField
var colorWeak: Int = 0
@BindColor(R.color.password_strength_bar_low)
@JvmField
var colorLow: Int = 0
@BindColor(R.color.password_strength_bar_ok)
@JvmField
var colorOk: Int = 0
@BindColor(R.color.password_strength_bar_strong)
@JvmField
var colorStrong: Int = 0
@IntRange(from = 0, to = 4)
var strength = 0
set(newValue) {
field = newValue.coerceIn(0, 4)
when (newValue) {
0 -> {
bar1.setBackgroundColor(colorBackground)
bar2.setBackgroundColor(colorBackground)
bar3.setBackgroundColor(colorBackground)
bar4.setBackgroundColor(colorBackground)
}
1 -> {
bar1.setBackgroundColor(colorWeak)
bar2.setBackgroundColor(colorBackground)
bar3.setBackgroundColor(colorBackground)
bar4.setBackgroundColor(colorBackground)
}
2 -> {
bar1.setBackgroundColor(colorLow)
bar2.setBackgroundColor(colorLow)
bar3.setBackgroundColor(colorBackground)
bar4.setBackgroundColor(colorBackground)
}
3 -> {
bar1.setBackgroundColor(colorOk)
bar2.setBackgroundColor(colorOk)
bar3.setBackgroundColor(colorOk)
bar4.setBackgroundColor(colorBackground)
}
4 -> {
bar1.setBackgroundColor(colorStrong)
bar2.setBackgroundColor(colorStrong)
bar3.setBackgroundColor(colorStrong)
bar4.setBackgroundColor(colorStrong)
}
}
}
init {
LayoutInflater.from(context)
.inflate(R.layout.view_password_strength_bar, this, true)
orientation = HORIZONTAL
ButterKnife.bind(this)
strength = 0
}
}

View File

@ -17,6 +17,7 @@
package im.vector.riotredesign.core.utils
import android.content.Context
import android.text.TextUtils
import timber.log.Timber
import java.io.File
@ -87,3 +88,46 @@ private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean {
return action.invoke(file)
}
/**
* Get the file extension of a fileUri or a filename
*
* @param fileUri the fileUri (can be a simple filename)
* @return the file extension, in lower case, or null is extension is not available or empty
*/
fun getFileExtension(fileUri: String): String? {
var reducedStr = fileUri
if (!TextUtils.isEmpty(reducedStr)) {
// Remove fragment
val fragment = fileUri.lastIndexOf('#')
if (fragment > 0) {
reducedStr = fileUri.substring(0, fragment)
}
// Remove query
val query = reducedStr.lastIndexOf('?')
if (query > 0) {
reducedStr = reducedStr.substring(0, query)
}
// Remove path
val filenamePos = reducedStr.lastIndexOf('/')
val filename = if (0 <= filenamePos) reducedStr.substring(filenamePos + 1) else reducedStr
// Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern
// See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs
if (!filename.isEmpty()) {
val dotPos = filename.lastIndexOf('.')
if (0 <= dotPos) {
val ext = filename.substring(dotPos + 1)
if (ext.isNotBlank()) {
return ext.toLowerCase()
}
}
}
}
return null
}

View File

@ -269,9 +269,10 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
val permissionsArrayToBeGranted = permissionsListToBeGranted.toTypedArray()
// for android < M, we use a custom dialog to request the contacts book access.
/*
if (permissionsListToBeGranted.contains(Manifest.permission.READ_CONTACTS)
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
TODO()
/*
AlertDialog.Builder(activity)
.setIcon(android.R.drawable.ic_dialog_info)
.setTitle(R.string.permissions_rationale_popup_title)
@ -293,13 +294,13 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
}
}
.show()
*/
} else {
fragment?.requestPermissions(permissionsArrayToBeGranted, requestCode)
?: run {
ActivityCompat.requestPermissions(activity, permissionsArrayToBeGranted, requestCode)
}
}
*/
} else {
// permissions were granted, start now.
isPermissionGranted = true

View File

@ -0,0 +1,113 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.restore
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import im.vector.fragments.keysbackup.restore.KeysBackupRestoreFromPassphraseFragment
import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.SimpleFragmentActivity
class KeysBackupRestoreActivity : SimpleFragmentActivity() {
companion object {
fun intent(context: Context): Intent {
return Intent(context, KeysBackupRestoreActivity::class.java)
}
}
override fun getTitleRes() = R.string.title_activity_keys_backup_restore
private lateinit var viewModel: KeysBackupRestoreSharedViewModel
override fun initUiAndData() {
super.initUiAndData()
viewModel = ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java)
viewModel.initSession(mSession)
viewModel.keyVersionResult.observe(this, Observer { keyVersion ->
if (keyVersion != null && supportFragmentManager.fragments.isEmpty()) {
val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData().privateKeySalt != null
if (isBackupCreatedFromPassphrase) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupRestoreFromPassphraseFragment.newInstance())
.commitNow()
} else {
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupRestoreFromKeyFragment.newInstance())
.commitNow()
}
}
})
viewModel.keyVersionResultError.observe(this, Observer { uxStateEvent ->
uxStateEvent?.getContentIfNotHandled()?.let {
AlertDialog.Builder(this)
.setTitle(R.string.unknown_error)
.setMessage(it)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
//nop
finish()
}
.show()
}
})
if (viewModel.keyVersionResult.value == null) {
//We need to fetch from API
viewModel.getLatestVersion(this)
}
viewModel.navigateEvent.observe(this, Observer { uxStateEvent ->
when (uxStateEvent?.getContentIfNotHandled()) {
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_RECOVER_WITH_KEY -> {
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupRestoreFromKeyFragment.newInstance())
.addToBackStack(null)
.commit()
}
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupRestoreSuccessFragment.newInstance())
.commit()
}
}
})
viewModel.loadingEvent.observe(this, Observer {
updateWaitingView(it)
})
viewModel.importRoomKeysFinishWithResult.observe(this, Observer {
it?.getContentIfNotHandled()?.let {
//set data?
setResult(Activity.RESULT_OK)
finish()
}
})
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.restore
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import butterknife.BindView
import butterknife.OnClick
import butterknife.OnTextChanged
import com.google.android.material.textfield.TextInputLayout
import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.startImportTextFromFileIntent
import timber.log.Timber
class KeysBackupRestoreFromKeyFragment : VectorBaseFragment() {
companion object {
fun newInstance() = KeysBackupRestoreFromKeyFragment()
private const val REQUEST_TEXT_FILE_GET = 1
}
override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_key
private lateinit var viewModel: KeysBackupRestoreFromKeyViewModel
private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel
@BindView(R.id.keys_backup_key_enter_til)
lateinit var mKeyInputLayout: TextInputLayout
@BindView(R.id.keys_restore_key_enter_edittext)
lateinit var mKeyTextEdit: EditText
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(KeysBackupRestoreFromKeyViewModel::class.java)
sharedViewModel = activity?.run {
ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
mKeyTextEdit.setText(viewModel.recoveryCode.value)
mKeyTextEdit.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
onRestoreFromKey()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
mKeyInputLayout.error = viewModel.recoveryCodeErrorText.value
viewModel.recoveryCodeErrorText.observe(this, Observer { newValue ->
mKeyInputLayout.error = newValue
})
}
@OnTextChanged(R.id.keys_restore_key_enter_edittext)
fun onRestoreKeyTextEditChange(s: Editable?) {
s?.toString()?.let {
viewModel.updateCode(it)
}
}
@OnClick(R.id.keys_restore_button)
fun onRestoreFromKey() {
val value = viewModel.recoveryCode.value
if (value.isNullOrBlank()) {
viewModel.recoveryCodeErrorText.value = context?.getString(R.string.keys_backup_recovery_code_empty_error_message)
} else {
viewModel.recoverKeys(context!!, sharedViewModel)
}
}
@OnClick(R.id.keys_backup_import)
fun onImport() {
startImportTextFromFileIntent(this, REQUEST_TEXT_FILE_GET)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_TEXT_FILE_GET && resultCode == Activity.RESULT_OK) {
val dataURI = data?.data
if (dataURI != null) {
try {
activity
?.contentResolver
?.openInputStream(dataURI)
?.bufferedReader()
?.use { it.readText() }
?.let {
mKeyTextEdit.setText(it)
mKeyTextEdit.setSelection(it.length)
}
} catch (e: Exception) {
Timber.e(e, "Failed to read recovery kay from text")
}
}
return
}
super.onActivityResult(requestCode, resultCode, data)
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.restore
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.StepProgressListener
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.core.ui.views.KeysBackupBanner
import timber.log.Timber
class KeysBackupRestoreFromKeyViewModel : ViewModel() {
var recoveryCode: MutableLiveData<String> = MutableLiveData()
var recoveryCodeErrorText: MutableLiveData<String> = MutableLiveData()
init {
recoveryCode.value = null
recoveryCodeErrorText.value = null
}
//========= Actions =========
fun updateCode(newValue: String) {
recoveryCode.value = newValue
recoveryCodeErrorText.value = null
}
fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) {
val session = sharedViewModel.session
val keysBackup = session.getKeysBackupService()
recoveryCodeErrorText.value = null
val recoveryKey = recoveryCode.value!!
val keysVersionResult = sharedViewModel.keyVersionResult.value!!
keysBackup.restoreKeysWithRecoveryKey(keysVersionResult,
recoveryKey,
null,
session.sessionParams.credentials.userId,
object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) {
when (step) {
is StepProgressListener.Step.DownloadingKey -> {
sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
+ "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message),
isIndeterminate = true)
}
is StepProgressListener.Step.ImportingKey -> {
// Progress 0 can take a while, display an indeterminate progress in this case
if (step.progress == 0) {
sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
+ "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
isIndeterminate = true)
} else {
sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
+ "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
step.progress,
step.total)
}
}
}
}
},
object : MatrixCallback<ImportRoomKeysResult> {
override fun onSuccess(info: ImportRoomKeysResult) {
sharedViewModel.loadingEvent.value = null
sharedViewModel.didRecoverSucceed(info)
KeysBackupBanner.onRecoverDoneForVersion(context, keysVersionResult.version!!)
trustOnDecrypt(keysBackup, keysVersionResult)
}
override fun onFailure(failure: Throwable) {
sharedViewModel.loadingEvent.value = null
recoveryCodeErrorText.value = context.getString(R.string.keys_backup_recovery_code_error_decrypt)
Timber.e(failure, "## onUnexpectedError")
}
})
}
private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) {
keysBackup.trustKeysBackupVersion(keysVersionResult, true,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.d("##### trustKeysBackupVersion onSuccess")
}
})
}
}

View File

@ -0,0 +1,135 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.fragments.keysbackup.restore
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.SpannableString
import android.text.style.ClickableSpan
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.core.text.set
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import butterknife.BindView
import butterknife.OnClick
import butterknife.OnTextChanged
import com.google.android.material.textfield.TextInputLayout
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
class KeysBackupRestoreFromPassphraseFragment : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_passphrase
private lateinit var viewModel: KeysBackupRestoreFromPassphraseViewModel
private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel
@BindView(R.id.keys_backup_passphrase_enter_til)
lateinit var mPassphraseInputLayout: TextInputLayout
@BindView(R.id.keys_backup_passphrase_enter_edittext)
lateinit var mPassphraseTextEdit: EditText
@BindView(R.id.keys_backup_view_show_password)
lateinit var mPassphraseReveal: ImageView
@BindView(R.id.keys_backup_passphrase_help_with_link)
lateinit var helperTextWithLink: TextView
@OnClick(R.id.keys_backup_view_show_password)
fun toggleVisibilityMode() {
viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false)
}
companion object {
fun newInstance() = KeysBackupRestoreFromPassphraseFragment()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(KeysBackupRestoreFromPassphraseViewModel::class.java)
sharedViewModel = activity?.run {
ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
viewModel.passphraseErrorText.observe(this, Observer { newValue ->
mPassphraseInputLayout.error = newValue
})
helperTextWithLink.text = spannableStringForHelperText(context!!)
viewModel.showPasswordMode.observe(this, Observer {
val shouldBeVisible = it ?: false
mPassphraseTextEdit.showPassword(shouldBeVisible)
mPassphraseReveal.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
})
mPassphraseTextEdit.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
onRestoreBackup()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
private fun spannableStringForHelperText(context: Context): SpannableString {
val clickableText = context.getString(R.string.keys_backup_restore_use_recovery_key)
val helperText = context.getString(R.string.keys_backup_restore_with_passphrase_helper_with_link, clickableText)
val spanString = SpannableString(helperText)
// used just to have default link representation
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View?) {}
}
val start = helperText.indexOf(clickableText)
val end = start + clickableText.length
spanString[start, end] = clickableSpan
return spanString
}
@OnTextChanged(R.id.keys_backup_passphrase_enter_edittext)
fun onPassphraseTextEditChange(s: Editable?) {
s?.toString()?.let { viewModel.updatePassphrase(it) }
}
@OnClick(R.id.keys_backup_passphrase_help_with_link)
fun onUseRecoveryKey() {
sharedViewModel.moveToRecoverWithKey()
}
@OnClick(R.id.keys_backup_restore_with_passphrase_submit)
fun onRestoreBackup() {
val value = viewModel.passphrase.value
if (value.isNullOrBlank()) {
viewModel.passphraseErrorText.value = context?.getString(R.string.passphrase_empty_error_message)
} else {
viewModel.recoverKeys(context!!, sharedViewModel)
}
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.restore
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.StepProgressListener
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.core.ui.views.KeysBackupBanner
import timber.log.Timber
class KeysBackupRestoreFromPassphraseViewModel : ViewModel() {
var passphrase: MutableLiveData<String> = MutableLiveData()
var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
var showPasswordMode: MutableLiveData<Boolean> = MutableLiveData()
init {
passphrase.value = null
passphraseErrorText.value = null
showPasswordMode.value = false
}
//========= Actions =========
fun updatePassphrase(newValue: String) {
passphrase.value = newValue
passphraseErrorText.value = null
}
fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) {
val keysBackup = sharedViewModel.session.getKeysBackupService()
passphraseErrorText.value = null
val keysVersionResult = sharedViewModel.keyVersionResult.value!!
keysBackup.restoreKeyBackupWithPassword(keysVersionResult,
passphrase.value!!,
null,
sharedViewModel.session.sessionParams.credentials.userId,
object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) {
when (step) {
is StepProgressListener.Step.ComputingKey -> {
sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
+ "\n" + context.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
step.progress,
step.total)
}
is StepProgressListener.Step.DownloadingKey -> {
sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
+ "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message),
isIndeterminate = true)
}
is StepProgressListener.Step.ImportingKey -> {
// Progress 0 can take a while, display an indeterminate progress in this case
if (step.progress == 0) {
sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
+ "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
isIndeterminate = true)
} else {
sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
+ "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
step.progress,
step.total)
}
}
}
}
},
object : MatrixCallback<ImportRoomKeysResult> {
override fun onSuccess(data: ImportRoomKeysResult) {
sharedViewModel.loadingEvent.value = null
sharedViewModel.didRecoverSucceed(data)
KeysBackupBanner.onRecoverDoneForVersion(context, keysVersionResult.version!!)
trustOnDecrypt(keysBackup, keysVersionResult)
}
override fun onFailure(failure: Throwable) {
sharedViewModel.loadingEvent.value = null
passphraseErrorText.value = context.getString(R.string.keys_backup_passphrase_error_decrypt)
Timber.e(failure, "## onUnexpectedError")
}
})
}
private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) {
keysBackup.trustKeysBackupVersion(keysVersionResult, true,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.d("##### trustKeysBackupVersion onSuccess")
}
})
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.fragments.keysbackup.restore
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.core.utils.LiveEvent
class KeysBackupRestoreSharedViewModel : ViewModel() {
companion object {
const val NAVIGATE_TO_RECOVER_WITH_KEY = "NAVIGATE_TO_RECOVER_WITH_KEY"
const val NAVIGATE_TO_SUCCESS = "NAVIGATE_TO_SUCCESS"
}
lateinit var session: Session
var keyVersionResult: MutableLiveData<KeysVersionResult> = MutableLiveData()
private var _keyVersionResultError: MutableLiveData<LiveEvent<String>> = MutableLiveData()
val keyVersionResultError: LiveData<LiveEvent<String>>
get() = _keyVersionResultError
private var _navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
val navigateEvent: LiveData<LiveEvent<String>>
get() = _navigateEvent
var loadingEvent: MutableLiveData<WaitingViewData> = MutableLiveData()
var importKeyResult: ImportRoomKeysResult? = null
var importRoomKeysFinishWithResult: MutableLiveData<LiveEvent<ImportRoomKeysResult>> = MutableLiveData()
init {
keyVersionResult.value = null
_keyVersionResultError.value = null
loadingEvent.value = null
}
fun initSession(session: Session) {
this.session = session
}
fun getLatestVersion(context: Context) {
val keysBackup = session.getKeysBackupService()
loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restore_is_getting_backup_version))
keysBackup.getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
override fun onSuccess(data: KeysVersionResult?) {
loadingEvent.value = null
if (data?.version.isNullOrBlank()) {
//should not happen
_keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, ""))
} else {
keyVersionResult.value = data
}
}
override fun onFailure(failure: Throwable) {
loadingEvent.value = null
_keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, failure.localizedMessage))
// TODO For network error
// _keyVersionResultError.value = LiveEvent(context.getString(R.string.network_error_please_check_and_retry))
}
})
}
fun moveToRecoverWithKey() {
_navigateEvent.value = LiveEvent(NAVIGATE_TO_RECOVER_WITH_KEY)
}
fun didRecoverSucceed(result: ImportRoomKeysResult) {
importKeyResult = result
_navigateEvent.value = LiveEvent(NAVIGATE_TO_SUCCESS)
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.restore
import android.os.Bundle
import android.widget.TextView
import androidx.lifecycle.ViewModelProviders
import butterknife.BindView
import butterknife.OnClick
import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.LiveEvent
class KeysBackupRestoreSuccessFragment : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_success
@BindView(R.id.keys_backup_restore_success)
lateinit var mSuccessText: TextView
@BindView(R.id.keys_backup_restore_success_info)
lateinit var mSuccessDetailsText: TextView
private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
sharedViewModel = activity?.run {
ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
sharedViewModel.importKeyResult?.let {
val part1 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part1,
it.totalNumberOfKeys, it.totalNumberOfKeys)
val part2 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part2,
it.successfullyNumberOfImportedKeys, it.successfullyNumberOfImportedKeys)
mSuccessDetailsText.text = String.format("%s\n%s", part1, part2)
}
//We don't put emoji in string xml as it will crash on old devices
mSuccessText.text = context?.getString(R.string.keys_backup_restore_success_title, "🎉")
}
@OnClick(R.id.keys_backup_setup_done_button)
fun onDone() {
sharedViewModel.importRoomKeysFinishWithResult.value = LiveEvent(sharedViewModel.importKeyResult!!)
}
companion object {
fun newInstance() = KeysBackupRestoreSuccessFragment()
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.settings
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import im.vector.fragments.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.SimpleFragmentActivity
class KeysBackupManageActivity : SimpleFragmentActivity() {
companion object {
fun intent(context: Context): Intent {
val intent = Intent(context, KeysBackupManageActivity::class.java)
return intent
}
}
override fun getTitleRes() = R.string.encryption_message_recovery
private lateinit var viewModel: KeysBackupSettingsViewModel
override fun initUiAndData() {
super.initUiAndData()
viewModel = ViewModelProviders.of(this).get(KeysBackupSettingsViewModel::class.java)
viewModel.initSession(mSession)
if (supportFragmentManager.fragments.isEmpty()) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupSettingsFragment.newInstance())
.commitNow()
mSession.getKeysBackupService()
.forceUsingLastVersion(object : MatrixCallback<Boolean> {})
}
viewModel.loadingEvent.observe(this, Observer {
updateWaitingView(it)
})
viewModel.apiResultError.observe(this, Observer { uxStateEvent ->
uxStateEvent?.getContentIfNotHandled()?.let {
AlertDialog.Builder(this)
.setTitle(R.string.unknown_error)
.setMessage(it)
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
}
})
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.fragments.keysbackup.settings
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
class KeysBackupSettingsFragment : VectorBaseFragment(),
KeysBackupSettingsRecyclerViewAdapter.AdapterListener {
companion object {
fun newInstance() = KeysBackupSettingsFragment()
}
override fun getLayoutResId() = R.layout.fragment_keys_backup_settings
private lateinit var viewModel: KeysBackupSettingsViewModel
@BindView(R.id.keys_backup_settings_recycler_view)
lateinit var recyclerView: RecyclerView
private var recyclerViewAdapter: KeysBackupSettingsRecyclerViewAdapter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = layoutManager
recyclerViewAdapter = KeysBackupSettingsRecyclerViewAdapter(activity!!)
recyclerView.adapter = recyclerViewAdapter
recyclerViewAdapter?.adapterListener = this
viewModel = activity?.run {
ViewModelProviders.of(this).get(KeysBackupSettingsViewModel::class.java)
} ?: throw Exception("Invalid Activity")
viewModel.keyBackupState.observe(this, Observer { keysBackupState ->
if (keysBackupState == null) {
//Cannot happen?
viewModel.keyVersionTrust.value = null
} else {
when (keysBackupState) {
KeysBackupState.Unknown,
KeysBackupState.CheckingBackUpOnHomeserver -> {
viewModel.loadingEvent.value = WaitingViewData("")
}
else -> {
viewModel.loadingEvent.value = null
//All this cases will be manage by looking at the backup trust object
viewModel.session?.getKeysBackupService()?.mKeysBackupVersion?.let {
viewModel.getKeysBackupTrust(it)
} ?: run {
viewModel.keyVersionTrust.value = null
}
}
}
}
// Update the adapter for each state change
viewModel.session?.let { session ->
recyclerViewAdapter?.updateWithTrust(session, viewModel.keyVersionTrust.value)
}
})
viewModel.keyVersionTrust.observe(this, Observer {
viewModel.session?.let { session ->
recyclerViewAdapter?.updateWithTrust(session, it)
}
})
}
override fun didSelectSetupMessageRecovery() {
context?.let {
startActivity(KeysBackupSetupActivity.intent(it, false))
}
}
override fun didSelectRestoreMessageRecovery() {
context?.let {
startActivity(KeysBackupRestoreActivity.intent(it))
}
}
override fun didSelectDeleteSetupMessageRecovery() {
activity?.let {
AlertDialog.Builder(it)
.setTitle(R.string.keys_backup_settings_delete_confirm_title)
.setMessage(R.string.keys_backup_settings_delete_confirm_message)
.setCancelable(false)
.setPositiveButton(R.string.keys_backup_settings_delete_confirm_title) { _, _ ->
viewModel.deleteCurrentBackup(it)
}
.setNegativeButton(R.string.cancel, null)
.setCancelable(true)
.show()
}
}
}

View File

@ -0,0 +1,233 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.fragments.keysbackup.settings
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.riotredesign.R
import im.vector.riotredesign.core.ui.list.GenericItemViewHolder
import im.vector.riotredesign.core.ui.list.GenericRecyclerViewItem
class KeysBackupSettingsRecyclerViewAdapter(val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val inflater: LayoutInflater = LayoutInflater.from(context)
private var infoList: List<GenericRecyclerViewItem> = ArrayList()
private var isBackupAlreadySetup = false
var adapterListener: AdapterListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
GenericItemViewHolder.resId -> GenericItemViewHolder(inflater.inflate(viewType, parent, false))
else -> FooterViewHolder(inflater.inflate(viewType, parent, false))
}
}
override fun getItemViewType(position: Int): Int {
return if (position < infoList.size) {
GenericItemViewHolder.resId
} else {
R.layout.item_keys_backup_settings_button_footer
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GenericItemViewHolder) {
holder.bind(infoList[position])
} else if (holder is FooterViewHolder) {
if (isBackupAlreadySetup) {
holder.button1.setText(R.string.keys_backup_settings_restore_backup_button)
holder.button1.isVisible = true
holder.button1.setOnClickListener {
adapterListener?.didSelectRestoreMessageRecovery()
}
holder.button2.setText(R.string.keys_backup_settings_delete_backup_button)
holder.button2.isVisible = true
holder.button2.setOnClickListener {
adapterListener?.didSelectDeleteSetupMessageRecovery()
}
} else {
holder.button1.setText(R.string.keys_backup_setup)
holder.button1.isVisible = true
holder.button1.setOnClickListener {
adapterListener?.didSelectSetupMessageRecovery()
}
holder.button2.isVisible = false
}
}
}
override fun getItemCount(): Int {
return infoList.size + 1 /*footer*/
}
fun updateWithTrust(session: Session, keyBackupVersionTrust: KeysBackupVersionTrust?) {
val keyBackupState = session.getKeysBackupService().state
val keyVersionResult = session.getKeysBackupService().mKeysBackupVersion
val infos = ArrayList<GenericRecyclerViewItem>()
var itemSummary: GenericRecyclerViewItem? = null
when (keyBackupState) {
KeysBackupState.Unknown,
KeysBackupState.CheckingBackUpOnHomeserver -> {
//In this cases recycler view is hidden any way
//so do nothing
}
KeysBackupState.Disabled -> {
itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_not_setup),
style = GenericRecyclerViewItem.STYLE.BIG_TEXT)
isBackupAlreadySetup = false
}
KeysBackupState.WrongBackUpVersion,
KeysBackupState.NotTrusted,
KeysBackupState.Enabling -> {
itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ko),
style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply {
description = keyBackupState.toString()
endIconResourceId = R.drawable.unit_test_ko
}
isBackupAlreadySetup = true
}
KeysBackupState.ReadyToBackUp -> {
itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ok),
style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply {
endIconResourceId = R.drawable.unit_test_ok
description = context.getString(R.string.keys_backup_info_keys_all_backup_up)
}
isBackupAlreadySetup = true
}
KeysBackupState.WillBackUp,
KeysBackupState.BackingUp -> {
itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ok),
style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply {
hasIndeterminateProcess = true
val totalKeys = session.inboundGroupSessionsCount(false)
?: 0
val backedUpKeys = session.inboundGroupSessionsCount(true)
?: 0
val remainingKeysToBackup = totalKeys - backedUpKeys
description = context.resources.getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup)
}
isBackupAlreadySetup = true
}
}
itemSummary?.let {
infos.add(it)
}
if (keyBackupVersionTrust != null) {
if (!keyBackupVersionTrust.usable) {
itemSummary?.description = context.getString(R.string.keys_backup_settings_untrusted_backup)
}
//Add infos
infos.add(GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_version), keyVersionResult?.version
?: ""))
infos.add(GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_algorithm), keyVersionResult?.algorithm
?: ""))
keyBackupVersionTrust.signatures.forEach {
val signatureInfo = GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_signature))
val isDeviceKnown = it.device != null
val isDeviceVerified = it.device?.isVerified ?: false
val isSignatureValid = it.valid
val deviceId: String = it.deviceId ?: ""
if (!isDeviceKnown) {
signatureInfo.description = context.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)
signatureInfo.endIconResourceId = R.drawable.e2e_warning
} else {
if (isSignatureValid) {
if (session.sessionParams.credentials.deviceId == it.deviceId) {
signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_this_device)
signatureInfo.endIconResourceId = R.drawable.e2e_verified
} else {
if (isDeviceVerified) {
signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)
signatureInfo.endIconResourceId = R.drawable.e2e_verified
} else {
signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)
signatureInfo.endIconResourceId = R.drawable.e2e_warning
}
}
} else {
//Invalid signature
signatureInfo.endIconResourceId = R.drawable.e2e_warning
if (isDeviceVerified) {
signatureInfo.description = context.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)
} else {
signatureInfo.description = context.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)
}
}
}
infos.add(signatureInfo)
} //end for each
}
infoList = infos
notifyDataSetChanged()
}
class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
ButterKnife.bind(this, itemView)
}
@BindView(R.id.keys_backup_settings_footer_button1)
lateinit var button1: Button
@BindView(R.id.keys_backup_settings_footer_button2)
lateinit var button2: Button
fun bind() {
}
}
interface AdapterListener {
fun didSelectSetupMessageRecovery()
fun didSelectRestoreMessageRecovery()
fun didSelectDeleteSetupMessageRecovery()
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.settings
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.core.utils.LiveEvent
class KeysBackupSettingsViewModel : ViewModel(),
KeysBackupService.KeysBackupStateListener {
var session: Session? = null
var keyVersionTrust: MutableLiveData<KeysBackupVersionTrust> = MutableLiveData()
var keyBackupState: MutableLiveData<KeysBackupState> = MutableLiveData()
private var _apiResultError: MutableLiveData<LiveEvent<String>> = MutableLiveData()
val apiResultError: LiveData<LiveEvent<String>>
get() = _apiResultError
var loadingEvent: MutableLiveData<WaitingViewData> = MutableLiveData()
fun initSession(session: Session) {
keyBackupState.value = session.getKeysBackupService().state
if (this.session == null) {
this.session = session
session.getKeysBackupService().addListener(this)
}
}
fun getKeysBackupTrust(versionResult: KeysVersionResult) {
val keysBackup = session?.getKeysBackupService()
keysBackup?.getKeysBackupTrust(versionResult, object : MatrixCallback<KeysBackupVersionTrust> {
override fun onSuccess(data: KeysBackupVersionTrust) {
keyVersionTrust.value = data
}
})
}
override fun onCleared() {
super.onCleared()
session?.getKeysBackupService()?.removeListener(this)
}
override fun onStateChange(newState: KeysBackupState) {
keyBackupState.value = newState
}
fun deleteCurrentBackup(context: Context) {
session?.getKeysBackupService()?.run {
loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_settings_deleting_backup))
if (currentBackupVersion != null) {
deleteBackup(currentBackupVersion!!, object : MatrixCallback<Unit> {
override fun onSuccess(info: Unit) {
//mmmm if state is stil unknown/checking..
loadingEvent.value = null
}
override fun onFailure(failure: Throwable) {
loadingEvent.value = null
_apiResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, failure.localizedMessage))
}
})
}
}
}
}

View File

@ -0,0 +1,206 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.setup
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.platform.SimpleFragmentActivity
class KeysBackupSetupActivity : SimpleFragmentActivity() {
override fun getTitleRes() = R.string.title_activity_keys_backup_setup
private lateinit var viewModel: KeysBackupSetupSharedViewModel
override fun initUiAndData() {
super.initUiAndData()
if (isFirstCreation()) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupSetupStep1Fragment.newInstance())
.commitNow()
}
viewModel = ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java)
viewModel.showManualExport.value = intent.getBooleanExtra(EXTRA_SHOW_MANUAL_EXPORT, false)
viewModel.initSession(mSession)
viewModel.isCreatingBackupVersion.observe(this, Observer {
val isCreating = it ?: false
if (isCreating) {
showWaitingView()
} else {
hideWaitingView()
}
})
viewModel.loadingStatus.observe(this, Observer {
it?.let {
updateWaitingView(it)
}
})
viewModel.navigateEvent.observe(this, Observer { uxStateEvent ->
when (uxStateEvent?.getContentIfNotHandled()) {
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2 -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupSetupStep2Fragment.newInstance())
.commit()
}
KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_3 -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.beginTransaction()
.replace(R.id.container, KeysBackupSetupStep3Fragment.newInstance())
.commit()
}
KeysBackupSetupSharedViewModel.NAVIGATE_FINISH -> {
val resultIntent = Intent()
viewModel.keysVersion.value?.version?.let {
resultIntent.putExtra(KEYS_VERSION, it)
}
setResult(RESULT_OK, resultIntent)
finish()
}
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> {
exportKeysManually()
}
}
})
viewModel.prepareRecoverFailError.observe(this, Observer { error ->
if (error != null) {
AlertDialog.Builder(this)
.setTitle(R.string.unknown_error)
.setMessage(error.localizedMessage)
.setPositiveButton(R.string.ok) { _, _ ->
//nop
viewModel.prepareRecoverFailError.value = null
}
.show()
}
})
viewModel.creatingBackupError.observe(this, Observer { error ->
if (error != null) {
AlertDialog.Builder(this)
.setTitle(R.string.unexpected_error)
.setMessage(error.localizedMessage)
.setPositiveButton(R.string.ok) { _, _ ->
//nop
viewModel.creatingBackupError.value = null
}
.show()
}
})
}
fun exportKeysManually() {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
notImplemented()
/*
showWaitingView()
CommonActivityUtils.exportKeys(mSession, passphrase, object : SimpleApiCallback<String>(this@KeysBackupSetupActivity) {
override fun onSuccess(filename: String) {
hideWaitingView()
AlertDialog.Builder(this@KeysBackupSetupActivity)
.setMessage(getString(R.string.encryption_export_saved_as, filename))
.setCancelable(false)
.setPositiveButton(R.string.ok) { dialog, which ->
val resultIntent = Intent()
resultIntent.putExtra(MANUAL_EXPORT, true)
setResult(RESULT_OK, resultIntent)
finish()
}
.show()
}
override fun onNetworkError(e: Exception) {
super.onNetworkError(e)
hideWaitingView()
}
override fun onMatrixError(e: MatrixError) {
super.onMatrixError(e)
hideWaitingView()
}
override fun onUnexpectedError(e: Exception) {
super.onUnexpectedError(e)
hideWaitingView()
}
})
*/
}
})
}
override fun onBackPressed() {
if (viewModel.shouldPromptOnBack) {
if (waitingView?.isVisible == true) {
return
}
AlertDialog.Builder(this)
.setTitle(R.string.keys_backup_setup_skip_title)
.setMessage(R.string.keys_backup_setup_skip_msg)
.setNegativeButton(R.string.stay, null)
.setPositiveButton(R.string.abort) { _, _ ->
finish()
}
.show()
} else {
super.onBackPressed()
}
}
// I think this code is useful, but it violates the code quality rules
// override fun onOptionsItemSelected(item: MenuItem): Boolean {
// if (item.itemId == android .R. id. home) {
// onBackPressed()
// return true
// }
//
// return super.onOptionsItemSelected(item)
// }
companion object {
const val KEYS_VERSION = "KEYS_VERSION"
const val MANUAL_EXPORT = "MANUAL_EXPORT"
const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_MANUAL_EXPORT"
fun intent(context: Context, showManualExport: Boolean): Intent {
val intent = Intent(context, KeysBackupSetupActivity::class.java)
intent.putExtra(EXTRA_SHOW_MANUAL_EXPORT, showManualExport)
return intent
}
}
}

View File

@ -0,0 +1,176 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.fragments.keysbackup.setup
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.nulabinc.zxcvbn.Strength
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.WaitingViewData
import im.vector.riotredesign.core.utils.LiveEvent
import timber.log.Timber
/**
* The shared view model between all fragments.
*/
class KeysBackupSetupSharedViewModel : ViewModel() {
companion object {
const val NAVIGATE_TO_STEP_2 = "NAVIGATE_TO_STEP_2"
const val NAVIGATE_TO_STEP_3 = "NAVIGATE_TO_STEP_3"
const val NAVIGATE_FINISH = "NAVIGATE_FINISH"
const val NAVIGATE_MANUAL_EXPORT = "NAVIGATE_MANUAL_EXPORT"
}
lateinit var session: Session
var showManualExport: MutableLiveData<Boolean> = MutableLiveData()
var navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var shouldPromptOnBack = true
// Step 2
var passphrase: MutableLiveData<String> = MutableLiveData()
var passphraseError: MutableLiveData<String> = MutableLiveData()
var confirmPassphrase: MutableLiveData<String> = MutableLiveData()
var confirmPassphraseError: MutableLiveData<String> = MutableLiveData()
var passwordStrength: MutableLiveData<Strength> = MutableLiveData()
var showPasswordMode: MutableLiveData<Boolean> = MutableLiveData()
// Step 3
// Var to ignore events from previous request(s) to generate a recovery key
private var currentRequestId: MutableLiveData<Long> = MutableLiveData()
var recoveryKey: MutableLiveData<String> = MutableLiveData()
var prepareRecoverFailError: MutableLiveData<Throwable> = MutableLiveData()
var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null
var copyHasBeenMade = false
var isCreatingBackupVersion: MutableLiveData<Boolean> = MutableLiveData()
var creatingBackupError: MutableLiveData<Throwable> = MutableLiveData()
var keysVersion: MutableLiveData<KeysVersion> = MutableLiveData()
var loadingStatus: MutableLiveData<WaitingViewData> = MutableLiveData()
init {
showPasswordMode.value = false
recoveryKey.value = null
isCreatingBackupVersion.value = false
prepareRecoverFailError.value = null
creatingBackupError.value = null
loadingStatus.value = null
}
fun initSession(session: Session) {
this.session = session
}
fun prepareRecoveryKey(context: Context, withPassphrase: String?) {
// Update requestId
currentRequestId.value = System.currentTimeMillis()
isCreatingBackupVersion.value = true
// Ensure passphrase is hidden during the process
showPasswordMode.value = false
recoveryKey.value = null
prepareRecoverFailError.value = null
session.let { mxSession ->
val requestedId = currentRequestId.value!!
mxSession.getKeysBackupService().prepareKeysBackupVersion(withPassphrase,
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
if (requestedId != currentRequestId.value) {
//this is an old request, we can't cancel but we can ignore
return
}
loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_step3_generating_key_status),
progress,
total)
}
},
object : MatrixCallback<MegolmBackupCreationInfo> {
override fun onSuccess(data: MegolmBackupCreationInfo) {
if (requestedId != currentRequestId.value) {
//this is an old request, we can't cancel but we can ignore
return
}
recoveryKey.value = data.recoveryKey
megolmBackupCreationInfo = data
copyHasBeenMade = false
val keyBackup = session?.getKeysBackupService()
if (keyBackup != null) {
createKeysBackup(context, keyBackup)
} else {
loadingStatus.value = null
isCreatingBackupVersion.value = false
prepareRecoverFailError.value = Exception()
}
}
override fun onFailure(failure: Throwable) {
if (requestedId != currentRequestId.value) {
//this is an old request, we can't cancel but we can ignore
return
}
loadingStatus.value = null
isCreatingBackupVersion.value = false
prepareRecoverFailError.value = failure ?: Exception()
}
})
}
}
private fun createKeysBackup(context: Context, keysBackup: KeysBackupService) {
loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_creating_backup), isIndeterminate = true)
creatingBackupError.value = null
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : MatrixCallback<KeysVersion> {
override fun onSuccess(data: KeysVersion) {
loadingStatus.value = null
isCreatingBackupVersion.value = false
keysVersion.value = data
navigateEvent.value = LiveEvent(NAVIGATE_TO_STEP_3)
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## createKeyBackupVersion")
loadingStatus.value = null
isCreatingBackupVersion.value = false
creatingBackupError.value = failure
}
})
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.setup
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import butterknife.BindView
import butterknife.OnClick
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.LiveEvent
class KeysBackupSetupStep1Fragment : VectorBaseFragment() {
companion object {
fun newInstance() = KeysBackupSetupStep1Fragment()
}
override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step1
private lateinit var viewModel: KeysBackupSetupSharedViewModel
@BindView(R.id.keys_backup_setup_step1_advanced)
lateinit var advancedOptionText: TextView
@BindView(R.id.keys_backup_setup_step1_manualExport)
lateinit var manualExportButton: Button
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = activity?.run {
ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
viewModel.showManualExport.observe(this, Observer {
val showOption = it ?: false
//Can't use isVisible because the kotlin compiler will crash with Back-end (JVM) Internal error: wrong code generated
advancedOptionText.visibility = if (showOption) View.VISIBLE else View.GONE
manualExportButton.visibility = if (showOption) View.VISIBLE else View.GONE
})
}
@OnClick(R.id.keys_backup_setup_step1_button)
fun onButtonClick() {
viewModel.navigateEvent.value = LiveEvent(KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2)
}
@OnClick(R.id.keys_backup_setup_step1_manualExport)
fun onManualExportClick() {
viewModel.navigateEvent.value = LiveEvent(KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT)
}
}

View File

@ -0,0 +1,212 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.setup
import android.os.AsyncTask
import android.os.Bundle
import android.text.TextUtils
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.OnClick
import butterknife.OnTextChanged
import com.google.android.material.textfield.TextInputLayout
import com.nulabinc.zxcvbn.Zxcvbn
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.ui.views.PasswordStrengthBar
import im.vector.riotredesign.features.settings.VectorLocale
class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step2
@BindView(R.id.keys_backup_root)
lateinit var rootGroup: ViewGroup
@BindView(R.id.keys_backup_passphrase_enter_edittext)
lateinit var mPassphraseTextEdit: EditText
@BindView(R.id.keys_backup_passphrase_enter_til)
lateinit var mPassphraseInputLayout: TextInputLayout
@BindView(R.id.keys_backup_view_show_password)
lateinit var mPassphraseReveal: ImageView
@BindView(R.id.keys_backup_passphrase_confirm_edittext)
lateinit var mPassphraseConfirmTextEdit: EditText
@BindView(R.id.keys_backup_passphrase_confirm_til)
lateinit var mPassphraseConfirmInputLayout: TextInputLayout
@BindView(R.id.keys_backup_passphrase_security_progress)
lateinit var mPassphraseProgressLevel: PasswordStrengthBar
private val zxcvbn = Zxcvbn()
@OnTextChanged(R.id.keys_backup_passphrase_enter_edittext)
fun onPassphraseChanged() {
viewModel.passphrase.value = mPassphraseTextEdit.text.toString()
viewModel.confirmPassphraseError.value = null
}
@OnTextChanged(R.id.keys_backup_passphrase_confirm_edittext)
fun onConfirmPassphraseChanged() {
viewModel.confirmPassphrase.value = mPassphraseConfirmTextEdit.text.toString()
}
private lateinit var viewModel: KeysBackupSetupSharedViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = activity?.run {
ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
viewModel.shouldPromptOnBack = true
bindViewToViewModel()
}
/* ==========================================================================================
* MENU
* ========================================================================================== */
private fun bindViewToViewModel() {
viewModel.passwordStrength.observe(this, Observer { strength ->
if (strength == null) {
mPassphraseProgressLevel.strength = 0
mPassphraseInputLayout.error = null
} else {
val score = strength.score
mPassphraseProgressLevel.strength = score
if (score in 1..3) {
val warning = strength.feedback?.getWarning(VectorLocale.applicationLocale)
if (warning != null) {
mPassphraseInputLayout.error = warning
}
val suggestions = strength.feedback?.getSuggestions(VectorLocale.applicationLocale)
if (suggestions != null) {
mPassphraseInputLayout.error = suggestions.firstOrNull()
}
} else {
mPassphraseInputLayout.error = null
}
}
})
viewModel.passphrase.observe(this, Observer<String> { newValue ->
if (TextUtils.isEmpty(newValue)) {
viewModel.passwordStrength.value = null
} else {
AsyncTask.execute {
val strength = zxcvbn.measure(newValue)
activity?.runOnUiThread {
viewModel.passwordStrength.value = strength
}
}
}
})
mPassphraseTextEdit.setText(viewModel.passphrase.value)
viewModel.passphraseError.observe(this, Observer {
TransitionManager.beginDelayedTransition(rootGroup)
mPassphraseInputLayout.error = it
})
mPassphraseConfirmTextEdit.setText(viewModel.confirmPassphrase.value)
viewModel.showPasswordMode.observe(this, Observer {
val shouldBeVisible = it ?: false
mPassphraseTextEdit.showPassword(shouldBeVisible)
mPassphraseConfirmTextEdit.showPassword(shouldBeVisible)
mPassphraseReveal.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
})
viewModel.confirmPassphraseError.observe(this, Observer {
TransitionManager.beginDelayedTransition(rootGroup)
mPassphraseConfirmInputLayout.error = it
})
mPassphraseConfirmTextEdit.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
doNext()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
@OnClick(R.id.keys_backup_view_show_password)
fun toggleVisibilityMode() {
viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false)
}
@OnClick(R.id.keys_backup_setup_step2_button)
fun doNext() {
when {
TextUtils.isEmpty(viewModel.passphrase.value) -> {
viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message)
}
viewModel.passphrase.value != viewModel.confirmPassphrase.value -> {
viewModel.confirmPassphraseError.value = context?.getString(R.string.passphrase_passphrase_does_not_match)
}
viewModel.passwordStrength.value?.score ?: 0 < 4 -> {
viewModel.passphraseError.value = context?.getString(R.string.passphrase_passphrase_too_weak)
}
else -> {
viewModel.megolmBackupCreationInfo = null
viewModel.prepareRecoveryKey(activity!!, viewModel.passphrase.value)
}
}
}
@OnClick(R.id.keys_backup_setup_step2_skip_button)
fun skipPassphrase() {
when {
TextUtils.isEmpty(viewModel.passphrase.value) -> {
// Generate a recovery key for the user
viewModel.megolmBackupCreationInfo = null
viewModel.prepareRecoveryKey(activity!!, null)
}
else -> {
// User has entered a passphrase but want to skip this step.
viewModel.passphraseError.value = context?.getString(R.string.keys_backup_passphrase_not_empty_error_message)
}
}
}
companion object {
fun newInstance() = KeysBackupSetupStep2Fragment()
}
}

View File

@ -0,0 +1,184 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.keysbackup.setup
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import butterknife.BindView
import butterknife.OnClick
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.*
import java.io.ByteArrayInputStream
class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step3
@BindView(R.id.keys_backup_setup_step3_button)
lateinit var mFinishButton: Button
@BindView(R.id.keys_backup_recovery_key_text)
lateinit var mRecoveryKeyTextView: TextView
@BindView(R.id.keys_backup_setup_step3_line2_text)
lateinit var mRecoveryKeyLabel2TextView: TextView
companion object {
fun newInstance() = KeysBackupSetupStep3Fragment()
}
private lateinit var viewModel: KeysBackupSetupSharedViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = activity?.run {
ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
viewModel.shouldPromptOnBack = false
viewModel.passphrase.observe(this, Observer {
if (it.isNullOrBlank()) {
//Recovery was generated, so show key and options to save
mRecoveryKeyLabel2TextView.text = getString(R.string.keys_backup_setup_step3_text_line2_no_passphrase)
mFinishButton.text = getString(R.string.keys_backup_setup_step3_button_title_no_passphrase)
mRecoveryKeyTextView.text = viewModel.recoveryKey.value!!
.replace(" ", "")
.chunked(16)
.joinToString("\n") {
it
.chunked(4)
.joinToString(" ")
}
mRecoveryKeyTextView.isVisible = true
} else {
mRecoveryKeyLabel2TextView.text = getString(R.string.keys_backup_setup_step3_text_line2)
mFinishButton.text = getString(R.string.keys_backup_setup_step3_button_title)
mRecoveryKeyTextView.isVisible = false
}
})
}
@OnClick(R.id.keys_backup_setup_step3_button)
fun onFinishButtonClicked() {
if (viewModel.megolmBackupCreationInfo == null) {
//nothing
} else {
if (viewModel.passphrase.value.isNullOrBlank() && !viewModel.copyHasBeenMade) {
Toast.makeText(context, R.string.keys_backup_setup_step3_please_make_copy, Toast.LENGTH_LONG).show()
} else {
viewModel.navigateEvent.value = LiveEvent(KeysBackupSetupSharedViewModel.NAVIGATE_FINISH)
}
}
}
@OnClick(R.id.keys_backup_setup_step3_copy_button)
fun onCopyButtonClicked() {
val dialog = BottomSheetDialog(activity!!)
dialog.setContentView(R.layout.bottom_sheet_save_recovery_key)
dialog.setCanceledOnTouchOutside(true)
val recoveryKey = viewModel.recoveryKey.value!!
if (viewModel.passphrase.value.isNullOrBlank()) {
dialog.findViewById<TextView>(R.id.keys_backup_recovery_key_text)?.isVisible = false
} else {
dialog.findViewById<TextView>(R.id.keys_backup_recovery_key_text)?.let {
it.isVisible = true
it.text = recoveryKey.replace(" ", "")
.chunked(16)
.joinToString("\n") {
it
.chunked(4)
.joinToString(" ")
}
}
}
dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener {
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
exportRecoveryKeyToFile(recoveryKey)
}
dialog.dismiss()
}
dialog.findViewById<View>(R.id.keys_backup_setup_share)?.setOnClickListener {
startSharePlainTextIntent(this,
context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title),
recoveryKey,
context?.getString(R.string.recovery_key))
viewModel.copyHasBeenMade = true
dialog.dismiss()
}
dialog.show()
}
@OnClick(R.id.keys_backup_recovery_key_text)
fun onRecoveryKeyClicked() {
viewModel.recoveryKey.value?.let {
viewModel.copyHasBeenMade = true
copyToClipboard(activity!!, it)
}
}
fun exportRecoveryKeyToFile(it: String) {
val stream = ByteArrayInputStream(it.toByteArray())
TODO()
/*
val url = viewModel.session.mediaCache.saveMedia(stream, "recovery-key" + System.currentTimeMillis() + ".txt", "text/plain")
stream.close()
CommonActivityUtils.saveMediaIntoDownloads(context,
File(Uri.parse(url).path!!), "recovery-key.txt", "text/plain", object : SimpleApiCallback<String>() {
override fun onSuccess(path: String) {
context?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
}
viewModel.copyHasBeenMade = true
}
})
*/
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
viewModel.recoveryKey.value?.let {
exportRecoveryKeyToFile(it)
}
}
}
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.verification
import android.content.Context
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.riotredesign.R
import im.vector.riotredesign.features.popup.PopupAlertManager
/**
* Listens to the VerificationManager and add a new notification when an incoming request is detected.
*/
class IncomingVerificationRequestHandler(val context: Context,
private val credentials: Credentials,
verificationService: SasVerificationService) : SasVerificationService.SasVerificationListener {
init {
verificationService.addListener(this)
}
override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) {
when (tx.state) {
SasVerificationTxState.OnStarted -> {
//Add a notification for every incoming request
val session = Matrix.getInstance().currentSession!!
val name = session.getUser(tx.otherUserId)?.displayName
?: tx.otherUserId
val alert = PopupAlertManager.VectorAlert(
"kvr_${tx.transactionId}",
context.getString(R.string.sas_incoming_request_notif_title),
context.getString(R.string.sas_incoming_request_notif_content, name),
R.drawable.shield
).apply {
contentAction = Runnable {
val intent = SASVerificationActivity.incomingIntent(context,
credentials.userId,
tx.otherUserId,
tx.transactionId)
weakCurrentActivity?.get()?.startActivity(intent)
}
dismissedAction = Runnable {
tx.cancel()
}
addButton(
context.getString(R.string.ignore),
Runnable {
tx.cancel()
}
)
addButton(
context.getString(R.string.action_open),
Runnable {
val intent = SASVerificationActivity.incomingIntent(context,
credentials.userId,
tx.otherUserId,
tx.transactionId)
weakCurrentActivity?.get()?.startActivity(intent)
}
)
//10mn expiration
expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L)
}
PopupAlertManager.postVectorAlert(alert)
}
SasVerificationTxState.Cancelled,
SasVerificationTxState.OnCancelled,
SasVerificationTxState.Verified -> {
//cancel related notification
PopupAlertManager.cancelAlert("kvr_${tx.transactionId}")
}
else -> Unit
}
}
override fun markedAsManuallyVerified(userId: String, deviceId: String) {
}
}

View File

@ -0,0 +1,245 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.verification
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.SimpleFragmentActivity
import im.vector.riotredesign.core.platform.WaitingViewData
class SASVerificationActivity : SimpleFragmentActivity() {
companion object {
private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
private const val EXTRA_TRANSACTION_ID = "EXTRA_TRANSACTION_ID"
private const val EXTRA_OTHER_USER_ID = "EXTRA_OTHER_USER_ID"
private const val EXTRA_OTHER_DEVICE_ID = "EXTRA_OTHER_DEVICE_ID"
private const val EXTRA_IS_INCOMING = "EXTRA_IS_INCOMING"
/* ==========================================================================================
* INPUT
* ========================================================================================== */
fun incomingIntent(context: Context, matrixID: String, otherUserId: String, transactionID: String): Intent {
val intent = Intent(context, SASVerificationActivity::class.java)
intent.putExtra(EXTRA_MATRIX_ID, matrixID)
intent.putExtra(EXTRA_TRANSACTION_ID, transactionID)
intent.putExtra(EXTRA_OTHER_USER_ID, otherUserId)
intent.putExtra(EXTRA_IS_INCOMING, true)
return intent
}
fun outgoingIntent(context: Context, matrixID: String, otherUserId: String, otherDeviceId: String): Intent {
val intent = Intent(context, SASVerificationActivity::class.java)
intent.putExtra(EXTRA_MATRIX_ID, matrixID)
intent.putExtra(EXTRA_OTHER_DEVICE_ID, otherDeviceId)
intent.putExtra(EXTRA_OTHER_USER_ID, otherUserId)
intent.putExtra(EXTRA_IS_INCOMING, false)
return intent
}
/* ==========================================================================================
* OUTPUT
* ========================================================================================== */
fun getOtherUserId(intent: Intent?): String? {
return intent?.getStringExtra(EXTRA_OTHER_USER_ID)
}
fun getOtherDeviceId(intent: Intent?): String? {
return intent?.getStringExtra(EXTRA_OTHER_DEVICE_ID)
}
}
override fun getTitleRes() = R.string.title_activity_verify_device
private lateinit var viewModel: SasVerificationViewModel
override fun initUiAndData() {
super.initUiAndData()
viewModel = ViewModelProviders.of(this).get(SasVerificationViewModel::class.java)
val transactionID: String? = intent.getStringExtra(EXTRA_TRANSACTION_ID)
if (isFirstCreation()) {
val isIncoming = intent.getBooleanExtra(EXTRA_IS_INCOMING, false)
if (isIncoming) {
//incoming always have a transaction id
viewModel.initIncoming(mSession, intent.getStringExtra(EXTRA_OTHER_USER_ID), transactionID)
} else {
viewModel.initOutgoing(mSession, intent.getStringExtra(EXTRA_OTHER_USER_ID), intent.getStringExtra(EXTRA_OTHER_DEVICE_ID))
}
if (isIncoming) {
val incoming = viewModel.transaction as IncomingSasVerificationTransaction
when (incoming.uxState) {
IncomingSasVerificationTransaction.UxState.UNKNOWN,
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT,
IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT -> {
supportActionBar?.setTitle(R.string.sas_incoming_request_title)
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
.replace(R.id.container, SASVerificationIncomingFragment.newInstance())
.commitNow()
}
IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION,
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
.replace(R.id.container, SASVerificationShortCodeFragment.newInstance())
.commitNow()
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
.replace(R.id.container, SASVerificationVerifiedFragment.newInstance())
.commitNow()
}
IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME,
IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER -> {
viewModel.navigateCancel()
}
}
} else {
val outgoing = viewModel.transaction as? OutgoingSasVerificationRequest
//transaction can be null, as not yet created
when (outgoing?.uxState) {
null,
OutgoingSasVerificationRequest.UxState.UNKNOWN,
OutgoingSasVerificationRequest.UxState.WAIT_FOR_START,
OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT -> {
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
.replace(R.id.container, SASVerificationStartFragment.newInstance())
.commitNow()
}
OutgoingSasVerificationRequest.UxState.SHOW_SAS,
OutgoingSasVerificationRequest.UxState.WAIT_FOR_VERIFICATION -> {
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
.replace(R.id.container, SASVerificationShortCodeFragment.newInstance())
.commitNow()
}
OutgoingSasVerificationRequest.UxState.VERIFIED -> {
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
.replace(R.id.container, SASVerificationVerifiedFragment.newInstance())
.commitNow()
}
OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME,
OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER -> {
viewModel.navigateCancel()
}
}
}
}
viewModel.navigateEvent.observe(this, Observer { uxStateEvent ->
when (uxStateEvent?.getContentIfNotHandled()) {
SasVerificationViewModel.NAVIGATE_FINISH -> {
finish()
}
SasVerificationViewModel.NAVIGATE_FINISH_SUCCESS -> {
val dataResult = Intent()
dataResult.putExtra(EXTRA_OTHER_DEVICE_ID, viewModel.otherDeviceId)
dataResult.putExtra(EXTRA_OTHER_USER_ID, viewModel.otherUserId)
setResult(Activity.RESULT_OK, dataResult)
finish()
}
SasVerificationViewModel.NAVIGATE_SAS_DISPLAY -> {
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out)
.replace(R.id.container, SASVerificationShortCodeFragment.newInstance())
.commitNow()
}
SasVerificationViewModel.NAVIGATE_SUCCESS -> {
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out)
.replace(R.id.container, SASVerificationVerifiedFragment.newInstance())
.commitNow()
}
SasVerificationViewModel.NAVIGATE_CANCELLED -> {
val isCancelledByMe = viewModel.transaction?.state == SasVerificationTxState.Cancelled
val humanReadableReason = when (viewModel.transaction?.cancelledReason) {
CancelCode.User -> getString(R.string.sas_error_m_user)
CancelCode.Timeout -> getString(R.string.sas_error_m_timeout)
CancelCode.UnknownTransaction -> getString(R.string.sas_error_m_unknown_transaction)
CancelCode.UnknownMethod -> getString(R.string.sas_error_m_unknown_method)
CancelCode.MismatchedCommitment -> getString(R.string.sas_error_m_mismatched_commitment)
CancelCode.MismatchedSas -> getString(R.string.sas_error_m_mismatched_sas)
CancelCode.UnexpectedMessage -> getString(R.string.sas_error_m_unexpected_message)
CancelCode.InvalidMessage -> getString(R.string.sas_error_m_invalid_message)
CancelCode.MismatchedKeys -> getString(R.string.sas_error_m_key_mismatch)
// Use user error
CancelCode.UserMismatchError -> getString(R.string.sas_error_m_user_error)
null -> getString(R.string.sas_error_unknown)
}
val message =
if (isCancelledByMe) getString(R.string.sas_cancelled_by_me, humanReadableReason)
else getString(R.string.sas_cancelled_by_other, humanReadableReason)
//Show a dialog
if (!this.isFinishing) {
AlertDialog.Builder(this)
.setTitle(R.string.sas_cancelled_dialog_title)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
//nop
finish()
}
.show()
}
}
}
})
viewModel.loadingLiveEvent.observe(this, Observer {
if (it == null) {
hideWaitingView()
} else {
val status = if (it == -1) "" else getString(it)
updateWaitingView(WaitingViewData(status, isIndeterminate = true))
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
//we want to cancel the transaction
viewModel.cancelTransaction()
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
//we want to cancel the transaction
viewModel.cancelTransaction()
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.verification
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import butterknife.BindView
import butterknife.OnClick
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.home.AvatarRenderer
class SASVerificationIncomingFragment : VectorBaseFragment() {
companion object {
fun newInstance() = SASVerificationIncomingFragment()
}
@BindView(R.id.sas_incoming_request_user_display_name)
lateinit var otherUserDisplayNameTextView: TextView
@BindView(R.id.sas_incoming_request_user_id)
lateinit var otherUserIdTextView: TextView
@BindView(R.id.sas_incoming_request_user_device)
lateinit var otherDeviceTextView: TextView
@BindView(R.id.sas_incoming_request_user_avatar)
lateinit var avatarImageView: ImageView
override fun getLayoutResId() = R.layout.fragment_sas_verification_incoming_request
private lateinit var viewModel: SasVerificationViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = activity?.run {
ViewModelProviders.of(this).get(SasVerificationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
otherUserDisplayNameTextView.text = viewModel.otherUser?.displayName ?: viewModel.otherUserId
otherUserIdTextView.text = viewModel.otherUserId
otherDeviceTextView.text = viewModel.otherDeviceId
viewModel.otherUser?.let {
AvatarRenderer.render(it, avatarImageView)
}
viewModel.transactionState.observe(this, Observer {
val uxState = (viewModel.transaction as? IncomingSasVerificationTransaction)?.uxState
when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
viewModel.loadingLiveEvent.value = null
}
IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT -> {
viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner
}
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
viewModel.shortCodeReady()
}
IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME,
IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER -> {
viewModel.loadingLiveEvent.value = null
viewModel.navigateCancel()
}
else -> Unit
}
})
}
@OnClick(R.id.sas_request_continue_button)
fun didAccept() {
viewModel.acceptTransaction()
}
@OnClick(R.id.sas_request_cancel_button)
fun didCancel() {
viewModel.cancelTransaction()
}
}

View File

@ -0,0 +1,183 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.verification
import android.os.Bundle
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import butterknife.BindView
import butterknife.OnClick
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
class SASVerificationShortCodeFragment : VectorBaseFragment() {
private lateinit var viewModel: SasVerificationViewModel
companion object {
fun newInstance() = SASVerificationShortCodeFragment()
}
@BindView(R.id.sas_decimal_code)
lateinit var decimalTextView: TextView
@BindView(R.id.sas_emoji_description)
lateinit var descriptionTextView: TextView
@BindView(R.id.sas_emoji_grid)
lateinit var emojiGrid: ViewGroup
@BindView(R.id.emoji0)
lateinit var emoji0View: ViewGroup
@BindView(R.id.emoji1)
lateinit var emoji1View: ViewGroup
@BindView(R.id.emoji2)
lateinit var emoji2View: ViewGroup
@BindView(R.id.emoji3)
lateinit var emoji3View: ViewGroup
@BindView(R.id.emoji4)
lateinit var emoji4View: ViewGroup
@BindView(R.id.emoji5)
lateinit var emoji5View: ViewGroup
@BindView(R.id.emoji6)
lateinit var emoji6View: ViewGroup
override fun getLayoutResId() = R.layout.fragment_sas_verification_display_code
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = activity?.run {
ViewModelProviders.of(this).get(SasVerificationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
viewModel.transaction?.let {
if (it.supportsEmoji()) {
val emojicodes = it.getEmojiCodeRepresentation()
emojicodes.forEachIndexed { index, emojiRepresentation ->
when (index) {
0 -> {
emoji0View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji0View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
1 -> {
emoji1View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji1View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
2 -> {
emoji2View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji2View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
3 -> {
emoji3View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji3View.findViewById<TextView>(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId)
}
4 -> {
emoji4View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji4View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
5 -> {
emoji5View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji5View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
6 -> {
emoji6View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
emoji6View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
}
}
}
}
//decimal is at least supported
decimalTextView.text = it.getDecimalCodeRepresentation()
if (it.supportsEmoji()) {
descriptionTextView.text = getString(R.string.sas_emoji_description)
decimalTextView.isVisible = false
emojiGrid.isVisible = true
} else {
descriptionTextView.text = getString(R.string.sas_decimal_description)
decimalTextView.isVisible = true
emojiGrid.isInvisible = true
}
}
viewModel.transactionState.observe(this, Observer {
if (viewModel.transaction is IncomingSasVerificationTransaction) {
val uxState = (viewModel.transaction as IncomingSasVerificationTransaction).uxState
when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
viewModel.loadingLiveEvent.value = null
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
viewModel.loadingLiveEvent.value = null
viewModel.deviceIsVerified()
}
IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME,
IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER -> {
viewModel.loadingLiveEvent.value = null
viewModel.navigateCancel()
}
else -> {
viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner
}
}
} else if (viewModel.transaction is OutgoingSasVerificationRequest) {
val uxState = (viewModel.transaction as OutgoingSasVerificationRequest).uxState
when (uxState) {
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
viewModel.loadingLiveEvent.value = null
}
OutgoingSasVerificationRequest.UxState.VERIFIED -> {
viewModel.loadingLiveEvent.value = null
viewModel.deviceIsVerified()
}
OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME,
OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER -> {
viewModel.loadingLiveEvent.value = null
viewModel.navigateCancel()
}
else -> {
viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner
}
}
}
})
}
@OnClick(R.id.sas_request_continue_button)
fun didAccept() {
viewModel.confirmEmojiSame()
}
@OnClick(R.id.sas_request_cancel_button)
fun didCancel() {
viewModel.cancelTransaction()
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.verification
import android.os.Bundle
import android.view.ViewGroup
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.OnClick
import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.core.platform.VectorBaseFragment
class SASVerificationStartFragment : VectorBaseFragment() {
companion object {
fun newInstance() = SASVerificationStartFragment()
}
override fun getLayoutResId() = R.layout.fragment_sas_verification_start
private lateinit var viewModel: SasVerificationViewModel
@BindView(R.id.rootLayout)
lateinit var rootLayout: ViewGroup
@BindView(R.id.sas_start_button)
lateinit var startButton: Button
@BindView(R.id.sas_start_button_loading)
lateinit var startButtonLoading: ProgressBar
@BindView(R.id.sas_verifying_keys)
lateinit var loadingText: TextView
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = activity?.run {
ViewModelProviders.of(this).get(SasVerificationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
viewModel.transactionState.observe(this, Observer {
val uxState = (viewModel.transaction as? OutgoingSasVerificationRequest)?.uxState
when (uxState) {
OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT -> {
//display loading
TransitionManager.beginDelayedTransition(this.rootLayout)
this.loadingText.isVisible = true
this.startButton.isInvisible = true
this.startButtonLoading.isVisible = true
this.startButtonLoading.animate()
}
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
viewModel.shortCodeReady()
}
OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME,
OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER -> {
viewModel.navigateCancel()
}
else -> {
TransitionManager.beginDelayedTransition(this.rootLayout)
this.loadingText.isVisible = false
this.startButton.isVisible = true
this.startButtonLoading.isVisible = false
}
}
})
}
@OnClick(R.id.sas_start_button)
fun doStart() {
viewModel.beginSasKeyVerification()
}
@OnClick(R.id.sas_legacy_verification)
fun doLegacy() {
(requireActivity() as VectorBaseActivity).notImplemented()
/*
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId
?: "", object : SimpleApiCallback<MXDeviceInfo>() {
override fun onSuccess(info: MXDeviceInfo?) {
info?.let {
CommonActivityUtils.displayDeviceVerificationDialogLegacy(it, it.userId, viewModel.session, activity, object : YesNoListener {
override fun yes() {
viewModel.manuallyVerified()
}
override fun no() {
}
})
}
}
})
*/
}
@OnClick(R.id.sas_cancel_button)
fun doCancel() {
// Transaction may be started, or not
viewModel.cancelTransaction()
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.verification
import android.os.Bundle
import androidx.lifecycle.ViewModelProviders
import butterknife.OnClick
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
class SASVerificationVerifiedFragment : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_sas_verification_verified
companion object {
fun newInstance() = SASVerificationVerifiedFragment()
}
private lateinit var viewModel: SasVerificationViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = activity?.run {
ViewModelProviders.of(this).get(SasVerificationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
@OnClick(R.id.sas_verification_verified_done_button)
fun onDone() {
viewModel.finishSuccess()
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.crypto.verification
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.core.utils.LiveEvent
class SasVerificationViewModel : ViewModel(),
SasVerificationService.SasVerificationListener {
companion object {
const val NAVIGATE_FINISH = "NAVIGATE_FINISH"
const val NAVIGATE_FINISH_SUCCESS = "NAVIGATE_FINISH_SUCCESS"
const val NAVIGATE_SAS_DISPLAY = "NAVIGATE_SAS_DISPLAY"
const val NAVIGATE_SUCCESS = "NAVIGATE_SUCCESS"
const val NAVIGATE_CANCELLED = "NAVIGATE_CANCELLED"
}
lateinit var sasVerificationService: SasVerificationService
var otherUserId: String? = null
var otherDeviceId: String? = null
var otherUser: User? = null
var transaction: SasVerificationTransaction? = null
var transactionState: MutableLiveData<SasVerificationTxState> = MutableLiveData()
init {
//Force a first observe
transactionState.value = null
}
private var _navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
val navigateEvent: LiveData<LiveEvent<String>>
get() = _navigateEvent
var loadingLiveEvent: MutableLiveData<Int> = MutableLiveData()
var transactionID: String? = null
set(value) {
if (value != null) {
transaction = sasVerificationService.getExistingTransaction(otherUserId!!, value)
transactionState.value = transaction?.state
otherDeviceId = transaction?.otherDeviceId
}
field = value
}
fun initIncoming(session: Session, otherUserId: String, transactionID: String?) {
this.sasVerificationService = session.getSasVerificationService()
this.otherUserId = otherUserId
this.transactionID = transactionID
this.sasVerificationService.addListener(this)
this.otherUser = session.getUser(otherUserId)
if (transactionID == null || transaction == null) {
//sanity, this transaction is not known anymore
_navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
}
}
fun initOutgoing(session: Session, otherUserId: String, otherDeviceId: String) {
this.sasVerificationService = session.getSasVerificationService()
this.otherUserId = otherUserId
this.otherDeviceId = otherDeviceId
this.sasVerificationService.addListener(this)
this.otherUser = session.getUser(otherUserId)
}
fun beginSasKeyVerification() {
val verificationSAS = sasVerificationService.beginKeyVerificationSAS(otherUserId!!, otherDeviceId!!)
this.transactionID = verificationSAS
}
override fun transactionCreated(tx: SasVerificationTransaction) {
}
override fun transactionUpdated(tx: SasVerificationTransaction) {
if (transactionID == tx.transactionId) {
transactionState.value = tx.state
}
}
override fun markedAsManuallyVerified(userId: String, deviceId: String) {
}
fun cancelTransaction() {
transaction?.cancel()
_navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
}
fun finishSuccess() {
_navigateEvent.value = LiveEvent(NAVIGATE_FINISH_SUCCESS)
}
fun manuallyVerified() {
if (otherUserId != null && otherDeviceId != null) {
sasVerificationService.markedLocallyAsManuallyVerified(otherUserId!!, otherDeviceId!!)
}
_navigateEvent.value = LiveEvent(NAVIGATE_FINISH_SUCCESS)
}
fun acceptTransaction() {
(transaction as? IncomingSasVerificationTransaction)?.performAccept()
}
fun confirmEmojiSame() {
transaction?.userHasVerifiedShortCode()
}
fun shortCodeReady() {
loadingLiveEvent.value = null
_navigateEvent.value = LiveEvent(NAVIGATE_SAS_DISPLAY)
}
fun deviceIsVerified() {
loadingLiveEvent.value = null
_navigateEvent.value = LiveEvent(NAVIGATE_SUCCESS)
}
fun navigateCancel() {
_navigateEvent.value = LiveEvent(NAVIGATE_CANCELLED)
}
override fun onCleared() {
super.onCleared()
sasVerificationService.removeListener(this)
}
}

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.glide.GlideRequest
@ -55,6 +56,11 @@ object AvatarRenderer {
render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView)
}
@UiThread
fun render(user: User, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), user.avatarUrl, user.userId, user.displayName, DrawableImageViewTarget(imageView))
}
@UiThread
fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView))

View File

@ -66,6 +66,8 @@ class HomeModule {
roomMemberItemFactory = RoomMemberItemFactory(get()),
roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()),
callItemFactory = CallItemFactory(get()),
encryptionItemFactory = EncryptionItemFactory(get()),
encryptedItemFactory = EncryptedItemFactory(get(), get(), messageItemFactory),
defaultItemFactory = DefaultItemFactory()
)
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)

View File

@ -0,0 +1,84 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail.timeline.factory
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.MXDecryptionException
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
class EncryptedItemFactory(
private val session: Session,
private val stringProvider: StringProvider,
private val messageItemFactory: MessageItemFactory) {
fun create(timelineEvent: TimelineEvent,
nextEvent: TimelineEvent?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
return when {
EventType.ENCRYPTED == timelineEvent.root.type -> {
val decrypted: MXEventDecryptionResult?
try {
decrypted = session.decryptEvent(timelineEvent.root, "TODO")
} catch (e: MXDecryptionException) {
val errorDescription =
if (e.cryptoError?.code == MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE) {
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
} else {
e.localizedMessage
}
val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
val spannableStr = SpannableString(message)
spannableStr.setSpan(StyleSpan(Typeface.ITALIC), 0, message.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// TODO This is not correct format for error, change it
return NoticeItem_()
.noticeText(spannableStr)
.avatarUrl(timelineEvent.senderAvatar)
.memberName(timelineEvent.senderName)
}
if (decrypted == null) {
return null
}
if (decrypted.mClearEvent == null) {
return null
}
val decryptedTimelineEvent = timelineEvent.copy(root = decrypted.mClearEvent!!)
// Success
return messageItemFactory.create(decryptedTimelineEvent, nextEvent, callback)
}
else -> null
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
class EncryptionItemFactory(private val stringProvider: StringProvider) {
fun create(event: TimelineEvent): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
return NoticeItem_()
.noticeText(text)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
return when {
EventType.ENCRYPTION == event.type -> {
val content = event.content.toModel<EncryptionEventContent>() ?: return null
stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm)
}
else -> null
}
}
}

View File

@ -28,6 +28,8 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
private val roomMemberItemFactory: RoomMemberItemFactory,
private val roomHistoryVisibilityItemFactory: RoomHistoryVisibilityItemFactory,
private val callItemFactory: CallItemFactory,
private val encryptionItemFactory: EncryptionItemFactory,
private val encryptedItemFactory: EncryptedItemFactory,
private val defaultItemFactory: DefaultItemFactory) {
fun create(event: TimelineEvent,
@ -46,8 +48,10 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> callItemFactory.create(event)
EventType.ENCRYPTED,
EventType.ENCRYPTION,
EventType.ENCRYPTION -> encryptionItemFactory.create(event)
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, callback)
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)

View File

@ -0,0 +1,47 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.lifecycle
import android.app.Activity
import android.app.Application
import android.os.Bundle
import im.vector.riotredesign.features.popup.PopupAlertManager
class VectorActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
PopupAlertManager.onNewActivityDisplayed(activity)
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityDestroyed(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
}

View File

@ -61,7 +61,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent {
Matrix.getInstance(context)?.defaultSession?.let { session ->
session.dataHandler
?.getRoom(roomId)
?.markAllAsRead(object : SimpleApiCallback<Void>() {
?.markAllAsRead(object : SimpleApiCallback<Unit>() {
override fun onSuccess(void: Void?) {
// Ignore
}
@ -100,7 +100,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent {
val event = Event(mxMessage, session.credentials.userId, roomId)
room.storeOutgoingEvent(event)
room.sendEvent(event, object : ApiCallback<Void?> {
room.sendEvent(event, object : MatrixCallback<Void?> {
override fun onSuccess(info: Void?) {
Timber.d("Send message : onSuccess ")
val notifiableMessageEvent = NotifiableMessageEvent(

View File

@ -0,0 +1,229 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.popup
import android.app.Activity
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import com.tapadoo.alerter.Alerter
import com.tapadoo.alerter.OnHideAlertListener
import im.vector.riotredesign.R
import im.vector.riotredesign.features.crypto.verification.SASVerificationActivity
import timber.log.Timber
import java.lang.ref.WeakReference
/**
* Responsible of displaying important popup alerts on top of the screen.
* Alerts are stacked and will be displayed sequentially
*/
object PopupAlertManager {
private var weakCurrentActivity: WeakReference<Activity>? = null
private var currentAlerter: VectorAlert? = null
private val alertFiFo = ArrayList<VectorAlert>()
fun postVectorAlert(alert: VectorAlert) {
synchronized(alertFiFo) {
alertFiFo.add(alert)
}
displayNextIfPossible()
}
fun cancelAlert(uid: String) {
synchronized(alertFiFo) {
alertFiFo.listIterator().apply {
while (this.hasNext()) {
val next = this.next()
if (next.uid == uid) {
this.remove()
}
}
}
}
//it could also be the current one
if (currentAlerter?.uid == uid) {
Alerter.hide()
currentIsDismissed()
}
}
fun onNewActivityDisplayed(activity: Activity) {
//we want to remove existing popup on previous activity and display it on new one
if (currentAlerter != null) {
weakCurrentActivity?.get()?.let {
Alerter.clearCurrent(it)
}
}
if (shouldIgnoreActivity(activity)) {
return
}
weakCurrentActivity = WeakReference(activity)
if (currentAlerter != null) {
if (currentAlerter!!.expirationTimestamp != null && System.currentTimeMillis() > currentAlerter!!.expirationTimestamp!!) {
//this alert has expired, remove it
//perform dismiss
try {
currentAlerter?.dismissedAction?.run()
} catch (e: Exception) {
Timber.e("## failed to perform action")
}
currentAlerter = null
Handler(Looper.getMainLooper()).postDelayed({
displayNextIfPossible()
}, 2000)
} else {
showAlert(currentAlerter!!, activity, animate = false)
}
} else {
Handler(Looper.getMainLooper()).postDelayed({
displayNextIfPossible()
}, 2000)
}
}
private fun shouldIgnoreActivity(activity: Activity) = activity is SASVerificationActivity
private fun displayNextIfPossible() {
val currentActivity = weakCurrentActivity?.get()
if (Alerter.isShowing || currentActivity == null) {
//will retry later
return
}
val next: VectorAlert?
synchronized(alertFiFo) {
next = alertFiFo.firstOrNull()
if (next != null) alertFiFo.remove(next)
}
currentAlerter = next
next?.let {
val currentTime = System.currentTimeMillis()
if (next.expirationTimestamp != null && currentTime > next.expirationTimestamp!!) {
//skip
try {
next.dismissedAction?.run()
} catch (e: java.lang.Exception) {
Timber.e("## failed to perform action")
}
displayNextIfPossible()
} else {
showAlert(it, currentActivity)
}
}
}
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {
alert.weakCurrentActivity = WeakReference(activity)
Alerter.create(activity)
.setTitle(alert.title)
.setText(alert.description)
.apply {
if (!animate) {
setEnterAnimation(R.anim.anim_alerter_no_anim)
}
alert.iconId?.let {
setIcon(it)
}
alert.actions.forEach { action ->
addButton(action.title, R.style.AlerterButton, View.OnClickListener {
if (action.autoClose) {
currentIsDismissed()
Alerter.hide()
}
try {
action.action.run()
} catch (e: java.lang.Exception) {
Timber.e("## failed to perform action")
}
})
}
setOnClickListener(View.OnClickListener { _ ->
alert.contentAction?.let {
currentIsDismissed()
Alerter.hide()
try {
it.run()
} catch (e: java.lang.Exception) {
Timber.e("## failed to perform action")
}
}
})
}
.setOnHideListener(OnHideAlertListener {
//called when dismissed on swipe
try {
alert.dismissedAction?.run()
} catch (e: java.lang.Exception) {
Timber.e("## failed to perform action")
}
currentIsDismissed()
})
.enableSwipeToDismiss()
.enableInfiniteDuration(true)
.setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color)
.show()
}
private fun currentIsDismissed() {
//current alert has been hidden
currentAlerter = null
Handler(Looper.getMainLooper()).postDelayed({
displayNextIfPossible()
}, 500)
}
/**
* Dataclass to describe an important alert with actions.
*/
data class VectorAlert(val uid: String,
val title: String,
val description: String,
@DrawableRes val iconId: Int?) {
data class Button(val title: String, val action: Runnable, val autoClose: Boolean)
//will be set by manager, and accessible by actions at runtime
var weakCurrentActivity: WeakReference<Activity>? = null
val actions = ArrayList<Button>()
var contentAction: Runnable? = null
var dismissedAction: Runnable? = null
/** If this timestamp is after current time, this alert will be skipped */
var expirationTimestamp: Long? = null
fun addButton(title: String, action: Runnable, autoClose: Boolean = true) {
actions.add(Button(title, action, autoClose))
}
@ColorRes
var colorRes: Int? = null
}
}

View File

@ -199,7 +199,7 @@ object BugReporter {
userId = session.sessionParams.credentials.userId
deviceId = session.sessionParams.credentials.deviceId ?: "undefined"
// TODO matrixSdkVersion = session.getVersion(true);
// TODO olmVersion = session.getCryptoVersion(context, true);
olmVersion = session.getCryptoVersion(context, true)
}
if (!mIsCancelled) {

View File

@ -231,7 +231,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorPreferenceFra
try {
isEnabled = !TextUtils.equals(actions[0] as String, BingRule.ACTION_DONT_NOTIFY)
} catch (e: Exception) {
Timber.e(LOG_TAG, "## refreshPreferences failed " + e.message, e)
Timber.e("## refreshPreferences failed " + e.message, e)
}
}

View File

@ -22,6 +22,7 @@ import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Typeface
import android.media.RingtoneManager
import android.net.Uri
import android.os.AsyncTask
@ -42,8 +43,15 @@ import androidx.core.view.isVisible
import androidx.preference.*
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.extensions.withArgs
import im.vector.riotredesign.core.platform.SimpleTextWatcher
@ -54,9 +62,13 @@ import im.vector.riotredesign.core.preference.UserAvatarPreference
import im.vector.riotredesign.core.preference.VectorPreference
import im.vector.riotredesign.core.utils.*
import im.vector.riotredesign.features.MainActivity
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.themes.ThemeUtils
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.lang.ref.WeakReference
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
@ -88,16 +100,16 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
private var mDisplayedEmails = ArrayList<String>()
private var mDisplayedPhoneNumber = ArrayList<String>()
// TODO private var mMyDeviceInfo: DeviceInfo? = null
private var mMyDeviceInfo: DeviceInfo? = null
// TODO private var mDisplayedPushers = ArrayList<Pusher>()
private var interactionListener: VectorSettingsFragmentInteractionListener? = null
// devices: device IDs and device names
// TODO private var mDevicesNameList: List<DeviceInfo> = ArrayList()
private var mDevicesNameList: List<DeviceInfo> = ArrayList()
// used to avoid requesting to enter the password for each deletion
private var mAccountPassword: String? = null
private var mAccountPassword: String = ""
// current publicised group list
private var mPublicisedGroups: MutableSet<String>? = null
@ -331,7 +343,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (null != newValue && newValue as Boolean != mSession.isURLPreviewEnabled) {
displayLoadingView()
mSession.setURLPreviewStatus(newValue, object : ApiCallback<Void> {
mSession.setURLPreviewStatus(newValue, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
it.isChecked = mSession.isURLPreviewEnabled
hideLoadingView()
@ -428,7 +440,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
displayLoadingView()
Matrix.getInstance(activity)?.pushManager?.forceSessionsRegistration(object : ApiCallback<Void> {
Matrix.getInstance(activity)?.pushManager?.forceSessionsRegistration(object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
hideLoadingView()
}
@ -497,7 +509,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
/* TODO
displayLoadingView()
mSession.enableCrypto(newValue, object : ApiCallback<Void> {
mSession.enableCrypto(newValue, object : MatrixCallback<Unit> {
private fun refresh() {
activity?.runOnUiThread {
hideLoadingView()
@ -622,7 +634,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
// olm version
findPreference(PreferencesManager.SETTINGS_OLM_VERSION_PREFERENCE_KEY)
// TODO .summary = mSession.getCryptoVersion(appContext, false)
.summary = mSession.getCryptoVersion(requireContext(), false)
// copyright
findPreference(PreferencesManager.SETTINGS_COPYRIGHT_PREFERENCE_KEY)
@ -819,7 +831,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
Matrix.getInstance(context)?.addNetworkEventListener(mNetworkListener)
mSession.myUser.refreshThirdPartyIdentifiers(object : SimpleApiCallback<Void>() {
mSession.myUser.refreshThirdPartyIdentifiers(object : SimpleApiCallback<Unit>() {
override fun onSuccess(info: Void?) {
// ensure that the activity still exists
// and the result is called in the right thread
@ -830,7 +842,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
}
})
Matrix.getInstance(context)?.pushManager?.refreshPushersList(Matrix.getInstance(context)?.sessions, object : SimpleApiCallback<Void>(activity) {
Matrix.getInstance(context)?.pushManager?.refreshPushersList(Matrix.getInstance(context)?.sessions, object : SimpleApiCallback<Unit>(activity) {
override fun onSuccess(info: Void?) {
refreshPushersList()
}
@ -1098,7 +1110,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
/* TODO
showPasswordLoadingView(true)
mSession.updatePassword(oldPwd, newPwd, object : ApiCallback<Void> {
mSession.updatePassword(oldPwd, newPwd, object : MatrixCallback<Unit> {
private fun onDone(@StringRes textResId: Int) {
showPasswordLoadingView(false)
@ -1172,7 +1184,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
// when using FCM
// need to register on servers
if (isConnected && pushManager.useFcm() && (pushManager.isServerRegistered || pushManager.isServerUnRegistered)) {
val listener = object : ApiCallback<Void> {
val listener = object : MatrixCallback<Unit> {
private fun onDone() {
activity?.runOnUiThread {
@ -1269,7 +1281,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
if (!TextUtils.equals(mSession.myUser.displayname, value)) {
displayLoadingView()
mSession.myUser.updateDisplayName(value, object : ApiCallback<Void> {
mSession.myUser.updateDisplayName(value, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
// refresh the settings value
PreferenceManager.getDefaultSharedPreferences(activity).edit {
@ -1359,7 +1371,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_CALL_RINGTONE -> {
REQUEST_CALL_RINGTONE -> {
val callRingtoneUri: Uri? = data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
val thisActivity = activity
if (callRingtoneUri != null && thisActivity != null) {
@ -1368,9 +1380,9 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
}
}
REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data)
REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList()
REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data)
REQUEST_LOCALE -> {
REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList()
REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data)
REQUEST_LOCALE -> {
activity?.let {
startActivity(it.intent)
it.finish()
@ -1394,7 +1406,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
override fun onUploadComplete(uploadId: String?, contentUri: String?) {
activity?.runOnUiThread {
mSession.myUser.updateAvatarUrl(contentUri, object : ApiCallback<Void> {
mSession.myUser.updateAvatarUrl(contentUri, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
onCommonDone(null)
refreshDisplay()
@ -1494,7 +1506,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
/* TODO
displayLoadingView()
mSession.myUser.delete3Pid(pid, object : ApiCallback<Void> {
mSession.myUser.delete3Pid(pid, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
when (pid.medium) {
ThreePid.MEDIUM_EMAIL -> refreshEmailsList()
@ -1564,7 +1576,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
notImplemented()
/* TODO
mSession.unIgnoreUsers(idsList, object : ApiCallback<Void> {
mSession.unIgnoreUsers(idsList, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
onCommonDone(null)
}
@ -1652,7 +1664,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
.setPositiveButton(R.string.remove)
{ _, _ ->
displayLoadingView()
pushManager.unregister(mSession, pusher, object : ApiCallback<Void> {
pushManager.unregister(mSession, pusher, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
refreshPushersList()
onCommonDone(null)
@ -1798,7 +1810,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
displayLoadingView()
mSession.myUser.requestEmailValidationToken(pid, object : ApiCallback<Void> {
mSession.myUser.requestEmailValidationToken(pid, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
activity?.runOnUiThread { showEmailValidationDialog(pid) }
}
@ -1834,7 +1846,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
.setTitle(R.string.account_email_validation_title)
.setMessage(R.string.account_email_validation_message)
.setPositiveButton(R.string._continue) { _, _ ->
mSession.myUser.add3Pid(pid, true, object : ApiCallback<Void> {
mSession.myUser.add3Pid(pid, true, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
it.runOnUiThread {
hideLoadingView()
@ -2153,7 +2165,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
// device name
if (null != aMyDeviceInfo) {
cryptoInfoDeviceNamePreference.summary = "TODO" // aMyDeviceInfo.display_name
cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
displayDeviceRenameDialog(aMyDeviceInfo)
@ -2162,7 +2174,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
cryptoInfoDeviceNamePreference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
override fun onPreferenceLongClick(preference: Preference): Boolean {
activity?.let { copyToClipboard(it, "TODO") } //aMyDeviceInfo.display_name) }
activity?.let { copyToClipboard(it, aMyDeviceInfo.displayName!!) }
return true
}
}
@ -2180,8 +2192,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
manageBackupPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
context?.let {
notImplemented()
// TODO startActivity(KeysBackupManageActivity.intent(it, mSession.myUserId))
startActivity(KeysBackupManageActivity.intent(it))
}
false
}
@ -2199,48 +2210,45 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
// crypto section: device key (fingerprint)
if (!TextUtils.isEmpty(deviceId) && !TextUtils.isEmpty(userId)) {
/* TODO
mSession.crypto?.getDeviceInfo(userId, deviceId, object : SimpleApiCallback<MXDeviceInfo>() {
override fun onSuccess(deviceInfo: MXDeviceInfo?) {
if (null != deviceInfo && !TextUtils.isEmpty(deviceInfo.fingerprint()) && null != activity) {
cryptoInfoTextPreference.summary = deviceInfo.getFingerprintHumanReadable()
mSession.getDeviceInfo(userId, deviceId, object : MatrixCallback<MXDeviceInfo?> {
override fun onSuccess(data: MXDeviceInfo?) {
if (null != data && !TextUtils.isEmpty(data.fingerprint()) && null != activity) {
cryptoInfoTextPreference.summary = data.getFingerprintHumanReadable()
cryptoInfoTextPreference.setOnPreferenceClickListener {
activity?.let { copyToClipboard(it, deviceInfo.fingerprint()) }
data.fingerprint()?.let {
copyToClipboard(requireActivity(), it)
}
true
}
}
}
})
*/
}
sendToUnverifiedDevicesPref.isChecked = false
/* TODO
mSession.crypto?.getGlobalBlacklistUnverifiedDevices(object : SimpleApiCallback<Boolean>() {
override fun onSuccess(status: Boolean) {
sendToUnverifiedDevicesPref.isChecked = status
mSession.getGlobalBlacklistUnverifiedDevices(object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
sendToUnverifiedDevicesPref.isChecked = data
}
})
sendToUnverifiedDevicesPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
mSession.crypto?.getGlobalBlacklistUnverifiedDevices(object : SimpleApiCallback<Boolean>() {
override fun onSuccess(status: Boolean) {
if (sendToUnverifiedDevicesPref.isChecked != status) {
mSession.crypto
?.setGlobalBlacklistUnverifiedDevices(sendToUnverifiedDevicesPref.isChecked, object : SimpleApiCallback<Void>() {
override fun onSuccess(info: Void?) {
mSession.getGlobalBlacklistUnverifiedDevices(object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
if (sendToUnverifiedDevicesPref.isChecked != data) {
mSession.setGlobalBlacklistUnverifiedDevices(sendToUnverifiedDevicesPref.isChecked, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
}
})
}
})
}
}
})
true
}
*/
}
//==============================================================================================================
@ -2269,32 +2277,20 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
}
}
/* TODO
mSession.getDevicesList(object : ApiCallback<DevicesListResponse> {
override fun onSuccess(info: DevicesListResponse) {
if (info.devices.isEmpty()) {
mSession.getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
if (data.devices?.isEmpty() == true) {
removeDevicesPreference()
} else {
buildDevicesSettings(info.devices)
buildDevicesSettings(data.devices!!)
}
}
override fun onNetworkError(e: Exception) {
override fun onFailure(failure: Throwable) {
removeDevicesPreference()
onCommonDone(e.message)
}
override fun onMatrixError(e: MatrixError) {
removeDevicesPreference()
onCommonDone(e.message)
}
override fun onUnexpectedError(e: Exception) {
removeDevicesPreference()
onCommonDone(e.message)
onCommonDone(failure.message)
}
})
*/
} else {
removeDevicesPreference()
removeCryptographyPreference()
@ -2314,7 +2310,6 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
var isNewList = true
val myDeviceId = mSession.sessionParams.credentials.deviceId
/* TODO
if (aDeviceInfoList.size == mDevicesNameList.size) {
isNewList = !mDevicesNameList.containsAll(aDeviceInfoList)
}
@ -2324,14 +2319,14 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
mDevicesNameList = aDeviceInfoList
// sort before display: most recent first
DeviceInfo.sortByLastSeen(mDevicesNameList)
mDevicesNameList.sortByLastSeen()
// start from scratch: remove the displayed ones
mDevicesListSettingsCategory.removeAll()
for (deviceInfo in mDevicesNameList) {
// set bold to distinguish current device ID
if (null != myDeviceId && myDeviceId == deviceInfo.device_id) {
if (null != myDeviceId && myDeviceId == deviceInfo.deviceId) {
mMyDeviceInfo = deviceInfo
typeFaceHighlight = Typeface.BOLD
} else {
@ -2343,16 +2338,16 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
mTypeface = typeFaceHighlight
}
if (null == deviceInfo.device_id && null == deviceInfo.display_name) {
if (null == deviceInfo.deviceId && null == deviceInfo.displayName) {
continue
} else {
if (null != deviceInfo.device_id) {
preference.title = deviceInfo.device_id
if (null != deviceInfo.deviceId) {
preference.title = deviceInfo.deviceId
}
// display name parameter can be null (new JSON API)
if (null != deviceInfo.display_name) {
preference.summary = deviceInfo.display_name
if (null != deviceInfo.displayName) {
preference.summary = deviceInfo.displayName
}
}
@ -2370,7 +2365,6 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
refreshCryptographyPreference(mMyDeviceInfo)
}
*/
}
/**
@ -2388,22 +2382,21 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
val layout = inflater.inflate(R.layout.dialog_device_details, null)
var textView = layout.findViewById<TextView>(R.id.device_id)
textView.text = "TODO"//aDeviceInfo.device_id
textView.text = aDeviceInfo.deviceId
// device name
textView = layout.findViewById(R.id.device_name)
val displayName = "TODO" // if (TextUtils.isEmpty(aDeviceInfo.display_name)) LABEL_UNAVAILABLE_DATA else aDeviceInfo.display_name
val displayName = if (TextUtils.isEmpty(aDeviceInfo.displayName)) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName
textView.text = displayName
// last seen info
textView = layout.findViewById(R.id.device_last_seen)
/* TODO
if (!TextUtils.isEmpty(aDeviceInfo.last_seen_ip)) {
val lastSeenIp = aDeviceInfo.last_seen_ip
if (!TextUtils.isEmpty(aDeviceInfo.lastSeenIp)) {
val lastSeenIp = aDeviceInfo.lastSeenIp
val dateFormatTime = SimpleDateFormat("HH:mm:ss")
val time = dateFormatTime.format(Date(aDeviceInfo.last_seen_ts))
val time = dateFormatTime.format(Date(aDeviceInfo.lastSeenTs))
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
val lastSeenTime = dateFormat.format(Date(aDeviceInfo.last_seen_ts)) + ", " + time
val lastSeenTime = dateFormat.format(Date(aDeviceInfo.lastSeenTs)) + ", " + time
val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
textView.text = lastSeenInfo
} else {
@ -2411,7 +2404,6 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
layout.findViewById<View>(R.id.device_last_seen_title).visibility = View.GONE
textView.visibility = View.GONE
}
*/
// title & icon
builder.setTitle(R.string.devices_details_dialog_title)
@ -2419,12 +2411,10 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
.setView(layout)
.setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) }
/* TODO
// disable the deletion for our own device
if (!TextUtils.equals(mSession.crypto?.myDevice?.deviceId, aDeviceInfo.device_id)) {
if (!TextUtils.equals(mSession.getMyDevice()?.deviceId, aDeviceInfo.deviceId)) {
builder.setNegativeButton(R.string.delete) { _, _ -> displayDeviceDeletionDialog(aDeviceInfo) }
}
*/
builder.setNeutralButton(R.string.cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
@ -2449,9 +2439,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.edit_text)
notImplemented()
/* TODO
input.setText(aDeviceInfoToRename.display_name)
input.setText(aDeviceInfoToRename.displayName)
AlertDialog.Builder(it)
.setTitle(R.string.devices_details_device_name)
@ -2461,8 +2449,8 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
val newName = input.text.toString()
mSession.setDeviceName(aDeviceInfoToRename.device_id, newName, object : ApiCallback<Void> {
override fun onSuccess(info: Void?) {
mSession.setDeviceName(aDeviceInfoToRename.deviceId!!, newName, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
// search which preference is updated
@ -2471,36 +2459,27 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
for (i in 0 until count) {
val pref = mDevicesListSettingsCategory.getPreference(i)
if (TextUtils.equals(aDeviceInfoToRename.device_id, pref.title)) {
if (TextUtils.equals(aDeviceInfoToRename.deviceId, pref.title)) {
pref.summary = newName
}
}
// detect if the updated device is the current account one
if (TextUtils.equals(cryptoInfoDeviceIdPreference.summary, aDeviceInfoToRename.device_id)) {
if (TextUtils.equals(cryptoInfoDeviceIdPreference.summary, aDeviceInfoToRename.deviceId)) {
cryptoInfoDeviceNamePreference.summary = newName
}
// Also change the display name in aDeviceInfoToRename, in case of multiple renaming
aDeviceInfoToRename.display_name = newName
aDeviceInfoToRename.displayName = newName
}
override fun onNetworkError(e: Exception) {
onCommonDone(e.localizedMessage)
}
override fun onMatrixError(e: MatrixError) {
onCommonDone(e.localizedMessage)
}
override fun onUnexpectedError(e: Exception) {
onCommonDone(e.localizedMessage)
override fun onFailure(failure: Throwable) {
onCommonDone(failure.localizedMessage)
}
})
}
.setNegativeButton(R.string.cancel, null)
.show()
*/
}
}
@ -2511,29 +2490,19 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
*/
private fun deleteDevice(deviceId: String) {
notImplemented()
/* TODO
// We have to manage registration flow first, to handle what is necessary to delete a devive
/*
displayLoadingView()
mSession.deleteDevice(deviceId, mAccountPassword, object : ApiCallback<Void> {
override fun onSuccess(info: Void?) {
mSession.deleteDevice(deviceId, mAccountPassword, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
refreshDevicesList() // force settings update
}
private fun onError(message: String) {
mAccountPassword = null
onCommonDone(message)
}
override fun onNetworkError(e: Exception) {
onError(e.localizedMessage)
}
override fun onMatrixError(e: MatrixError) {
onError(e.localizedMessage)
}
override fun onUnexpectedError(e: Exception) {
onError(e.localizedMessage)
override fun onFailure(failure: Throwable) {
mAccountPassword = ""
onCommonDone(failure.localizedMessage)
}
})
*/
@ -2546,12 +2515,9 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
* @param aDeviceInfoToDelete device info
*/
private fun displayDeviceDeletionDialog(aDeviceInfoToDelete: DeviceInfo) {
notImplemented()
/*
TODO
if (aDeviceInfoToDelete.device_id != null) {
if (aDeviceInfoToDelete.deviceId != null) {
if (!TextUtils.isEmpty(mAccountPassword)) {
deleteDevice(aDeviceInfoToDelete.device_id)
deleteDevice(aDeviceInfoToDelete.deviceId!!)
} else {
activity?.let {
val inflater = it.layoutInflater
@ -2568,7 +2534,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
return@OnClickListener
}
mAccountPassword = passwordEditText.text.toString()
deleteDevice(aDeviceInfoToDelete.device_id)
deleteDevice(aDeviceInfoToDelete.deviceId!!)
})
.setNegativeButton(R.string.cancel) { _, _ ->
hideLoadingView()
@ -2587,21 +2553,20 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
} else {
Timber.e("## displayDeviceDeletionDialog(): sanity check failure")
}
*/
}
/**
* Manage the e2e keys export.
*/
private fun exportKeys() {
notImplemented()
// We need WRITE_EXTERNAL permission
/*
TODO
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
activity?.let { activity ->
ExportKeysDialog().show(activity, object : ExportKeysDiaLog.ExportKeyDialogListener {
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
notImplemented()
/*
displayLoadingView()
CommonActivityUtils.exportKeys(mSession, passphrase, object : SimpleApiCallback<String>(activity) {
@ -2630,11 +2595,11 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
hideLoadingView()
}
})
*/
}
})
}
}
*/
}
/**
@ -2642,8 +2607,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
*/
@SuppressLint("NewApi")
private fun importKeys() {
notImplemented()
// TODO activity?.let { openFileSelection(it, this, false, REQUEST_E2E_FILE_REQUEST_CODE) }
activity?.let { openFileSelection(it, this, false, REQUEST_E2E_FILE_REQUEST_CODE) }
}
/**
@ -2657,103 +2621,100 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
return
}
notImplemented()
/*
TODO
val sharedDataItems = ArrayList(RoomMediaMessage.listRoomMediaMessages(intent))
val thisActivity = activity
val sharedDataItems = ArrayList(RoomMediaMessage.listRoomMediaMessages(intent))
val thisActivity = activity
if (sharedDataItems.isNotEmpty() && thisActivity != null) {
val sharedDataItem = sharedDataItems[0]
val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null)
val builder = AlertDialog.Builder(thisActivity)
.setTitle(R.string.encryption_import_room_keys)
.setView(dialogLayout)
if (sharedDataItems.isNotEmpty() && thisActivity != null) {
val sharedDataItem = sharedDataItems[0]
val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null)
val builder = AlertDialog.Builder(thisActivity)
.setTitle(R.string.encryption_import_room_keys)
.setView(dialogLayout)
val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button)
val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button)
passPhraseEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
passPhraseEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text)
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text)
}
override fun afterTextChanged(s: Editable) {
override fun afterTextChanged(s: Editable) {
}
})
}
})
val importDialog = builder.show()
val appContext = thisActivity.applicationContext
val importDialog = builder.show()
val appContext = thisActivity.applicationContext
importButton.setOnClickListener(View.OnClickListener {
val password = passPhraseEditText.text.toString()
val resource = ResourceUtils.openResource(appContext, sharedDataItem.uri, sharedDataItem.getMimeType(appContext))
importButton.setOnClickListener(View.OnClickListener {
val password = passPhraseEditText.text.toString()
val resource = openResource(appContext, sharedDataItem.uri, sharedDataItem.getMimeType(appContext))
val data: ByteArray
if(resource?.mContentStream == null) {
appContext.toast("Error")
try {
data = ByteArray(resource.mContentStream.available())
resource.mContentStream.read(data)
resource.mContentStream.close()
} catch (e: Exception) {
try {
resource.mContentStream.close()
} catch (e2: Exception) {
Timber.e("## importKeys() : " + e2.message, e2)
return@OnClickListener
}
val data: ByteArray
try {
data = ByteArray(resource.mContentStream!!.available())
resource!!.mContentStream!!.read(data)
resource!!.mContentStream!!.close()
} catch (e: Exception) {
try {
resource!!.mContentStream!!.close()
} catch (e2: Exception) {
Timber.e(e2, "## importKeys()")
}
appContext.toast(e.localizedMessage)
return@OnClickListener
}
displayLoadingView()
mSession.importRoomKeys(data,
password,
null,
object : MatrixCallback<ImportRoomKeysResult> {
override fun onSuccess(info: ImportRoomKeysResult) {
if (!isAdded) {
return
}
hideLoadingView()
info?.let {
AlertDialog.Builder(thisActivity)
.setMessage(getString(R.string.encryption_import_room_keys_success,
it.successfullyNumberOfImportedKeys,
it.totalNumberOfKeys))
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
.show()
}
}
appContext.toast(e.localizedMessage)
override fun onFailure(failure: Throwable) {
appContext.toast(failure.localizedMessage)
hideLoadingView()
}
})
return@OnClickListener
}
displayLoadingView()
mSession.crypto?.importRoomKeys(data,
password,
null,
object : ApiCallback<ImportRoomKeysResult> {
override fun onSuccess(info: ImportRoomKeysResult?) {
if (!isAdded) {
return
}
hideLoadingView()
info?.let {
AlertDialog.Builder(thisActivity)
.setMessage(getString(R.string.encryption_import_room_keys_success,
it.successfullyNumberOfImportedKeys,
it.totalNumberOfKeys))
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
.show()
}
}
override fun onNetworkError(e: Exception) {
appContext.toast(e.localizedMessage)
hideLoadingView()
}
override fun onMatrixError(e: MatrixError) {
appContext.toast(e.localizedMessage)
hideLoadingView()
}
override fun onUnexpectedError(e: Exception) {
appContext.toast(e.localizedMessage)
hideLoadingView()
}
})
importDiaTimber.dismiss()
})
}
*/
importDialog.dismiss()
})
}
*/
}
//==============================================================================================================
@ -2776,7 +2737,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
/*
TODO
mSession.groupsManager.getUserPublicisedGroups(mSession.myUserId, true, object : ApiCallback<Set<String>> {
mSession.groupsManager.getUserPublicisedGroups(mSession.myUserId, true, object : MatrixCallback<Set<String>> {
override fun onSuccess(publicisedGroups: Set<String>) {
// clear everything
mGroupsFlairCategory.removeAll()
@ -2849,7 +2810,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
if (newValue != isFlaired) {
displayLoadingView()
mSession.groupsManager.updateGroupPublicity(group.groupId, newValue, object : ApiCallback<Void> {
mSession.groupsManager.updateGroupPublicity(group.groupId, newValue, object : MatrixCallback<Unit> {
override fun onSuccess(info: Void?) {
hideLoadingView()
if (newValue) {
@ -2884,15 +2845,10 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
}
*/
// TODO refreshCryptographyPreference(mMyDeviceInfo)
refreshCryptographyPreference(mMyDeviceInfo)
}
}
// TODO Remove
class DeviceInfo {
}
private class ClearMediaCacheAsyncTask internal constructor(
backgroundTask: () -> Unit,
onCompleteTask: () -> Unit

View File

@ -16,6 +16,7 @@
package im.vector.riotredesign.features.workers.signout
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
@ -30,13 +31,18 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.ButterKnife
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import org.koin.android.ext.android.inject
@ -99,13 +105,13 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
setupClickableView.setOnClickListener {
context?.let { context ->
// TODO startActivityForResult(KeysBackupSetupActivity.intent(context, getExtraMatrixID(), true), EXPORT_REQ)
startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
}
}
activateClickableView.setOnClickListener {
context?.let { context ->
// TODO startActivity(KeysBackupManageActivity.intent(context, getExtraMatrixID()))
startActivity(KeysBackupManageActivity.intent(context))
}
}
@ -119,20 +125,19 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
.setTitle(R.string.are_you_sure)
.setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages)
.setPositiveButton(R.string.backup) { _, _ ->
/* TODO
when (viewModel.keysBackupState.value) {
KeysBackupStateManager.KeysBackupState.NotTrusted -> {
KeysBackupState.NotTrusted -> {
context?.let { context ->
startActivity(KeysBackupManageActivity.intent(context, getExtraMatrixID()))
startActivity(KeysBackupManageActivity.intent(context))
}
}
KeysBackupStateManager.KeysBackupState.Disabled -> {
KeysBackupState.Disabled -> {
context?.let { context ->
startActivityForResult(KeysBackupSetupActivity.intent(context, getExtraMatrixID(), true), EXPORT_REQ)
startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
}
}
KeysBackupStateManager.KeysBackupState.BackingUp,
KeysBackupStateManager.KeysBackupState.WillBackUp -> {
KeysBackupState.BackingUp,
KeysBackupState.WillBackUp -> {
//keys are already backing up please wait
context?.toast(R.string.keys_backup_is_not_finished_please_wait)
}
@ -140,7 +145,6 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
//nop
}
}
*/
}
.setNegativeButton(R.string.action_sign_out) { _, _ ->
onSignOut?.run()
@ -165,60 +169,57 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
}
})
/* TODO
viewModel.keysBackupState.observe(this, Observer {
if (viewModel.keysExportedToFile.value == true) {
//ignore this
return@Observer
}
TransitionManager.beginDelayedTransition(rootLayout)
when (it) {
KeysBackupStateManager.KeysBackupState.ReadyToBackUp -> {
signoutClickableView.isVisible = true
dontWantClickableView.isVisible = false
setupClickableView.isVisible = false
activateClickableView.isVisible = false
backingUpStatusGroup.isVisible = true
backupProgress.isVisible = false
backupCompleteImage.isVisible = true
backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up)
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
viewModel.keysBackupState.observe(this, Observer {
if (viewModel.keysExportedToFile.value == true) {
//ignore this
return@Observer
}
KeysBackupStateManager.KeysBackupState.BackingUp,
KeysBackupStateManager.KeysBackupState.WillBackUp -> {
backingUpStatusGroup.isVisible = true
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
dontWantClickableView.isVisible = true
setupClickableView.isVisible = false
activateClickableView.isVisible = false
TransitionManager.beginDelayedTransition(rootLayout)
when (it) {
KeysBackupState.ReadyToBackUp -> {
signoutClickableView.isVisible = true
dontWantClickableView.isVisible = false
setupClickableView.isVisible = false
activateClickableView.isVisible = false
backingUpStatusGroup.isVisible = true
backupProgress.isVisible = true
backupCompleteImage.isVisible = false
backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys)
backupProgress.isVisible = false
backupCompleteImage.isVisible = true
backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up)
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
}
KeysBackupState.BackingUp,
KeysBackupState.WillBackUp -> {
backingUpStatusGroup.isVisible = true
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
dontWantClickableView.isVisible = true
setupClickableView.isVisible = false
activateClickableView.isVisible = false
backupProgress.isVisible = true
backupCompleteImage.isVisible = false
backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys)
}
KeysBackupState.NotTrusted -> {
backingUpStatusGroup.isVisible = false
dontWantClickableView.isVisible = true
setupClickableView.isVisible = false
activateClickableView.isVisible = true
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backup_not_active)
}
else -> {
backingUpStatusGroup.isVisible = false
dontWantClickableView.isVisible = true
setupClickableView.isVisible = true
activateClickableView.isVisible = false
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
}
}
KeysBackupStateManager.KeysBackupState.NotTrusted -> {
backingUpStatusGroup.isVisible = false
dontWantClickableView.isVisible = true
setupClickableView.isVisible = false
activateClickableView.isVisible = true
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backup_not_active)
}
else -> {
backingUpStatusGroup.isVisible = false
dontWantClickableView.isVisible = true
setupClickableView.isVisible = true
activateClickableView.isVisible = false
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
}
}
// updateSignOutSection()
})
*/
// updateSignOutSection()
})
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -244,14 +245,12 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
/* TODO
if (resultCode == Activity.RESULT_OK) {
if (requestCode == EXPORT_REQ) {
val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false)
viewModel.keysExportedToFile.value = manualExportDone
}
}
*/
}
}

View File

@ -19,12 +19,14 @@ package im.vector.riotredesign.features.workers.signout
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
class SignOutViewModel : ViewModel() { // TODO, KeysBackupStateManager.KeysBackupStateListener {
class SignOutViewModel : ViewModel(), KeysBackupService.KeysBackupStateListener {
// Keys exported manually
var keysExportedToFile = MutableLiveData<Boolean>()
// var keysBackupState = MutableLiveData<KeysBackupStateManager.KeysBackupState>()
var keysBackupState = MutableLiveData<KeysBackupState>()
private var mxSession: Session? = null
@ -32,60 +34,52 @@ class SignOutViewModel : ViewModel() { // TODO, KeysBackupStateManager.KeysBacku
if (mxSession == null) {
mxSession = session
// TODO
//mxSession?.crypto
// ?.keysBackup
// ?.addListener(this)
mxSession?.getKeysBackupService()
?.addListener(this)
}
//keysBackupState.value = mxSession?.crypto
// ?.keysBackup
// ?.state
keysBackupState.value = mxSession?.getKeysBackupService()
?.state
}
// /**
// * Safe way to get the current KeysBackup version
// */
// fun getCurrentBackupVersion(): String {
// return mxSession
// ?.crypto
// ?.keysBackup
// ?.currentBackupVersion
// ?: ""
// }
//
// /**
// * Safe way to get the number of keys to backup
// */
// fun getNumberOfKeysToBackup(): Int {
// return mxSession
// ?.crypto
// ?.cryptoStore
// ?.inboundGroupSessionsCount(false)
// ?: 0
// }
//
// /**
// * Safe way to tell if there are more keys on the server
// */
// fun canRestoreKeys(): Boolean {
// return mxSession
// ?.crypto
// ?.keysBackup
// ?.canRestoreKeys() == true
// }
//
// override fun onCleared() {
// super.onCleared()
//
// mxSession?.crypto
// ?.keysBackup
// ?.removeListener(this)
// }
//
// override fun onStateChange(newState: KeysBackupStateManager.KeysBackupState) {
// keysBackupState.value = newState
// }
/**
* Safe way to get the current KeysBackup version
*/
fun getCurrentBackupVersion(): String {
return mxSession
?.getKeysBackupService()
?.currentBackupVersion
?: ""
}
/**
* Safe way to get the number of keys to backup
*/
fun getNumberOfKeysToBackup(): Int {
return mxSession
?.inboundGroupSessionsCount(false)
?: 0
}
/**
* Safe way to tell if there are more keys on the server
*/
fun canRestoreKeys(): Boolean {
return mxSession
?.getKeysBackupService()
?.canRestoreKeys() == true
}
override fun onCleared() {
super.onCleared()
mxSession?.getKeysBackupService()
?.removeListener(this)
}
override fun onStateChange(newState: KeysBackupState) {
keysBackupState.value = newState
}
companion object {
/**
@ -94,17 +88,12 @@ class SignOutViewModel : ViewModel() { // TODO, KeysBackupStateManager.KeysBacku
fun doYouNeedToBeDisplayed(session: Session?): Boolean {
return false
/* TODO
return session
?.crypto
?.cryptoStore
?.inboundGroupSessionsCount(false)
?: 0 > 0
&& session
?.crypto
?.keysBackup
?.state != KeysBackupStateManager.KeysBackupState.ReadyToBackUp
*/
?.getKeysBackupService()
?.state != KeysBackupState.ReadyToBackUp
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="0"
android:interpolator="@anim/interpolator_slight_overshoot">
<translate
android:fromYDelta="0%"
android:toYDelta="0%" />
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/default_animation_duration"
android:shareInterpolator="false">
<alpha
android:fromAlpha="0"
android:toAlpha="1" />
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="@integer/default_animation_duration"
android:fromXDelta="-100%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="@integer/default_animation_duration"
android:fromXDelta="100%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/default_animation_duration"
android:shareInterpolator="false">
<alpha
android:fromAlpha="1"
android:toAlpha="0" />
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="@integer/default_animation_duration"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="-100%"
android:toYDelta="0%" />
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="@integer/default_animation_duration"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="100%"
android:toYDelta="0%" />
</set>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/default_animation_duration"
android:shareInterpolator="false">
</set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/waiting_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?vctr_waiting_background_color"
android:visibility="gone"
tools:visibility="visible">
<LinearLayout
android:id="@+id/waiting_view_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_horizontal_margin"
android:background="?attr/colorBackgroundFloating"
android:orientation="vertical"
android:padding="@dimen/layout_horizontal_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="@dimen/dialog_width_ratio">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/waiting_view_status_circular_progress"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp" />
<TextView
android:id="@+id/waiting_view_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:visibility="gone"
tools:text="Waiting status..."
tools:visibility="visible" />
</LinearLayout>
<ProgressBar
android:id="@+id/waiting_view_status_horizontal_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginTop="10dp"
android:visibility="gone"
tools:max="100"
tools:progress="30"
tools:visibility="visible" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorBackgroundFloating"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/keys_backup_setup_step3_copy_button_title"
android:textAlignment="center"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:id="@+id/keys_backup_recovery_key_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/layout_horizontal_margin"
android:fontFamily="monospace"
android:textAlignment="center"
android:textSize="20sp"
tools:text="HHWJ Y8DK RDR4\nBQEN FQ4V M4F8\nBQEN FQ4V M4A8" />
<LinearLayout
android:id="@+id/keys_backup_setup_share"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:src="@drawable/ic_material_share"
android:tint="?colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/keys_backup_setup_step3_share_recovery_file"
android:textSize="17sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/keys_backup_setup_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:src="@drawable/ic_material_save"
android:tint="?colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/keys_backup_setup_step3_save_button_title"
android:textSize="17sp" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<TextView
android:id="@+id/delete_dialog_info_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_delete_dialog_text"
android:textSize="12sp" />
<TextView
android:id="@+id/password_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_delete_pswd"
android:textSize="12sp"
android:textStyle="bold" />
<EditText
android:id="@+id/delete_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPassword"
android:maxLines="1" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/encryption_information_verify_device_warning"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/encryption_information_device_name"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/encrypted_device_info_device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device name" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/encryption_information_device_id"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/encrypted_device_info_device_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device id" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/encryption_information_device_key"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/encrypted_device_info_device_key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device key" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/encryption_information_verify_device_warning2"
android:textSize="12sp" />
</LinearLayout>
</ScrollView>

View File

@ -18,13 +18,13 @@
android:text="@string/encryption_export_notice"
android:textSize="16sp" />
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColorHint="?attr/vctr_default_text_hint_color">
<android.support.design.widget.TextInputEditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_e2e_keys_passphrase_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -32,10 +32,10 @@
android:inputType="textPassword"
android:textColor="?android:textColorPrimary" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/dialog_e2e_keys_confirm_passphrase_til"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -43,7 +43,7 @@
android:textColorHint="?attr/vctr_default_text_hint_color"
app:errorEnabled="true">
<android.support.design.widget.TextInputEditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_e2e_keys_confirm_passphrase_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -51,7 +51,7 @@
android:inputType="textPassword"
android:textColor="?android:textColorPrimary" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/dialog_e2e_keys_export_button"

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding"
android:paddingBottom="12dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColorHint="?attr/vctr_default_text_hint_color">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_e2e_keys_passphrase_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase_enter_passphrase"
android:inputType="textPassword"
android:textColor="?android:textColorPrimary" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/dialog_e2e_keys_import_button"
style="@style/VectorButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:enabled="false"
android:text="@string/encryption_import_import" />
</LinearLayout>

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/keys_backup_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/keys_backup_shield"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:src="@drawable/key_big"
android:tint="?android:textColorTertiary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/keys_restore_with_key"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/keys_backup_restore_with_recovery_key"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_shield" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/keys_backup_key_enter_til"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toStartOf="@+id/keys_backup_import"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_restore_with_key">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/keys_restore_key_enter_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/keys_backup_restore_key_enter_hint"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions|textMultiLine"
android:maxLines="3"
android:textColor="?android:textColorPrimary"
tools:text="EsTy 7CiZ Zqpj eqFq Wjz1 kzfS 59DE uZyA wt7b rhBE viyt kb1p" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/keys_backup_import"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_import_black"
android:tint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/keys_backup_key_enter_til"
app:layout_constraintTop_toTopOf="@id/keys_backup_key_enter_til" />
<TextView
android:id="@+id/keys_restore_key_help_with_link"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/keys_backup_restore_with_key_helper"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_key_enter_til" />
<Button
android:id="@+id/keys_restore_button"
style="@style/VectorButtonStyle"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:minWidth="200dp"
android:padding="16dp"
android:text="@string/keys_backup_unlock_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_restore_key_help_with_link"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/keys_backup_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/keys_backup_shield"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:src="@drawable/key_big"
android:tint="?android:textColorTertiary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/keys_backup_restore_with_passphrase"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/keys_backup_restore_with_passphrase"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/keys_backup_shield" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/keys_backup_passphrase_enter_til"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toStartOf="@id/keys_backup_view_show_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/keys_backup_restore_with_passphrase">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/keys_backup_passphrase_enter_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase_enter_passphrase"
android:maxLines="3"
android:singleLine="false"
android:textColor="?android:textColorPrimary"
tools:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/keys_backup_view_show_password"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye_black"
android:tint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/keys_backup_passphrase_enter_til"
app:layout_constraintTop_toTopOf="@+id/keys_backup_passphrase_enter_til" />
<TextView
android:id="@+id/keys_backup_passphrase_help_with_link"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_passphrase_enter_til"
tools:text="@string/keys_backup_restore_with_passphrase_helper_with_link" />
<Button
android:id="@+id/keys_backup_restore_with_passphrase_submit"
style="@style/VectorButtonStyle"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:minWidth="200dp"
android:padding="16dp"
android:text="@string/keys_backup_unlock_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_passphrase_help_with_link" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/keys_backup_shield"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:src="@drawable/key_big"
android:tint="?android:textColorTertiary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/keys_backup_restore_success"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textAlignment="center"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_shield"
tools:text="@string/keys_backup_restore_success_title" />
<TextView
android:id="@+id/keys_backup_restore_success_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textAlignment="center"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_restore_success"
tools:text="@string/keys_backup_restore_success_description" />
<Button
android:id="@+id/keys_backup_setup_done_button"
style="@style/VectorButtonStyle"
android:layout_margin="20dp"
android:minWidth="200dp"
android:padding="16dp"
android:text="@string/done"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_restore_success_info" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/keys_backup_settings_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="@dimen/layout_vertical_margin"
android:paddingBottom="@dimen/layout_vertical_margin_big"
tools:listitem="@layout/item_notification_troubleshoot" />

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".features.crypto.keysbackup.setup.KeysBackupSetupStep1Fragment">
<ImageView
android:id="@+id/keys_backup_setup_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:src="@drawable/backup_keys"
android:tint="?android:attr/textColorTertiary"
app:layout_constraintBottom_toTopOf="@+id/keys_backup_setup_step1_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.1"
app:layout_constraintVertical_chainStyle="spread" />
<TextView
android:id="@+id/keys_backup_setup_step1_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/keys_backup_setup_step1_title"
android:textAlignment="center"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/keys_backup_setup_step1_description"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_setup_image"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/keys_backup_setup_step1_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/keys_backup_setup_step1_description"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="@+id/keys_backup_setup_step1_button"
app:layout_constraintTop_toBottomOf="@id/keys_backup_setup_step1_title" />
<!-- Centered button -->
<Button
android:id="@+id/keys_backup_setup_step1_button"
style="@style/VectorButtonStyle"
android:layout_margin="16dp"
android:minWidth="200dp"
android:padding="8dp"
android:text="@string/keys_backup_setup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/keys_backup_setup_step1_description" />
<TextView
android:id="@+id/keys_backup_setup_step1_advanced"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginLeft="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:text="@string/keys_backup_setup_step1_advanced"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/keys_backup_setup_step1_button"
tools:visibility="visible" />
<Button
android:id="@+id/keys_backup_setup_step1_manualExport"
style="@style/VectorButtonStyleFlat"
android:layout_margin="16dp"
android:minWidth="200dp"
android:padding="8dp"
android:text="@string/keys_backup_setup_step1_manual_export"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/keys_backup_setup_step1_advanced"
app:layout_constraintVertical_bias="0"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/keys_backup_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".features.crypto.keysbackup.setup.KeysBackupSetupStep2Fragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/keys_backup_setup"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/keys_backup_setup_step2_text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text="@string/keys_backup_setup_step2_text_title"
android:textAlignment="center"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/keys_backup_setup_step2_text_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text="@string/keys_backup_setup_step2_text_description"
android:textAlignment="center"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_setup_step2_text_title" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/keys_backup_passphrase_enter_til"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toStartOf="@id/keys_backup_view_show_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/keys_backup_setup_step2_text_description">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/keys_backup_passphrase_enter_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase_create_passphrase"
android:maxLines="3"
android:textColor="?android:textColorPrimary" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/keys_backup_view_show_password"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye_black"
android:tint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/keys_backup_passphrase_enter_til"
app:layout_constraintTop_toTopOf="@id/keys_backup_passphrase_enter_til" />
<im.vector.riotredesign.core.ui.views.PasswordStrengthBar
android:id="@+id/keys_backup_passphrase_security_progress"
android:layout_width="0dp"
android:layout_height="4dp"
app:layout_constraintEnd_toEndOf="@id/keys_backup_passphrase_enter_til"
app:layout_constraintStart_toStartOf="@id/keys_backup_passphrase_enter_til"
app:layout_constraintTop_toBottomOf="@id/keys_backup_passphrase_enter_til" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/keys_backup_passphrase_confirm_til"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_weight="1"
app:errorEnabled="true"
app:layout_constraintEnd_toStartOf="@id/keys_backup_view_show_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/keys_backup_passphrase_security_progress">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/keys_backup_passphrase_confirm_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase_confirm_passphrase"
android:maxLines="3"
android:textColor="?android:textColorPrimary" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/keys_backup_setup_step2_button"
style="@style/VectorButtonStyle"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="@string/keys_backup_setup_step2_button_title"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/keys_backup_passphrase_confirm_til" />
<TextView
android:id="@+id/keys_backup_setup_recovery_key_alternative"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="@string/keys_backup_setup_step1_recovery_key_alternative"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@+id/keys_backup_setup_step2_button" />
<Button
android:id="@+id/keys_backup_setup_step2_skip_button"
style="@style/VectorButtonStyleFlat"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="@string/keys_backup_setup_step2_skip_button_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/keys_backup_setup_recovery_key_alternative" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/keys_backup_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".features.crypto.keysbackup.setup.KeysBackupSetupStep3Fragment">
<LinearLayout
android:id="@+id/keys_backup_setup_step3_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/keys_backup_setup_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:src="@drawable/backup_keys"
android:tint="?android:attr/textColorTertiary" />
<TextView
android:id="@+id/keys_backup_setup_step3_success_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginTop="20dp"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:text="@string/keys_backup_setup_step3_success_title"
android:textAlignment="center"
android:textSize="17sp"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/keys_backup_setup_step3_line1_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginTop="20dp"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:text="@string/keys_backup_setup_step3_text_line1"
android:textAlignment="center"
android:textSize="15sp" />
<TextView
android:id="@+id/keys_backup_setup_step3_line2_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginTop="20dp"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:textAlignment="center"
android:textSize="15sp"
tools:text="@string/keys_backup_setup_step3_text_line2" />
<TextView
android:id="@+id/keys_backup_recovery_key_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_horizontal_margin"
android:fontFamily="monospace"
android:textAlignment="center"
android:textSize="20sp"
android:visibility="gone"
tools:text="HHWJ Y8DK RDR4\nBQEN FQ4V M4F8\nBQEN FQ4V M4A8"
tools:visibility="visible" />
<Button
android:id="@+id/keys_backup_setup_step3_copy_button"
style="@style/VectorButtonStyleFlat"
android:layout_gravity="center"
android:text="@string/keys_backup_setup_step3_copy_button_title" />
<Button
android:id="@+id/keys_backup_setup_step3_button"
style="@style/VectorButtonStyle"
android:layout_gravity="center"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin"
android:minWidth="200dp"
tools:text="@string/keys_backup_setup_step3_button_title" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/sas_emoji_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_horizontal_margin"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:gravity="center"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/sas_emoji_description" />
<TextView
android:id="@+id/sas_emoji_description_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:gravity="center"
android:text="@string/sas_security_advise"
android:textColor="?android:attr/textColorSecondary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_emoji_description" />
<TextView
android:id="@+id/sas_decimal_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="28sp"
android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid"
tools:text="1234-4320-3905"
tools:visibility="visible" />
<GridLayout
android:id="@+id/sas_emoji_grid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:columnCount="@integer/number_of_emoji_per_line"
android:padding="@dimen/layout_vertical_margin"
android:useDefaultMargins="true"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_description_2"
tools:visibility="visible">
<include
android:id="@+id/emoji0"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji1"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji2"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji3"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji4"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji5"
layout="@layout/item_emoji_verif" />
<include
android:id="@+id/emoji6"
layout="@layout/item_emoji_verif" />
</GridLayout>
<Button
android:id="@+id/sas_request_continue_button"
style="@style/VectorButtonStyle"
android:layout_margin="@dimen/layout_vertical_margin"
android:minWidth="160dp"
android:text="@string/_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid" />
<Button
android:id="@+id/sas_request_cancel_button"
style="@style/VectorButtonStyleFlat"
android:layout_margin="@dimen/layout_vertical_margin"
android:text="@string/cancel"
app:layout_constraintEnd_toStartOf="@+id/sas_request_continue_button"
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/sas_incoming_request_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="@string/sas_incoming_request_title"
android:textAlignment="center"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/sas_incoming_request_user_avatar"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="@dimen/layout_vertical_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_incoming_request_title"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/sas_incoming_request_user_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_incoming_request_user_avatar"
tools:text="User name" />
<TextView
android:id="@+id/sas_incoming_request_user_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_incoming_request_user_display_name"
tools:text="\@foo:matrix.org" />
<TextView
android:id="@+id/sas_incoming_request_user_device"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_incoming_request_user_id"
tools:text="Device: Mobile" />
<TextView
android:id="@+id/sas_incoming_request_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:text="@string/sas_incoming_request_description"
android:textAlignment="center"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_incoming_request_user_device" />
<TextView
android:id="@+id/sas_incoming_request_description_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:text="@string/sas_incoming_request_description_2"
android:textAlignment="center"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_incoming_request_description" />
<Button
android:id="@+id/sas_request_continue_button"
style="@style/VectorButtonStyle"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:layout_marginEnd="@dimen/layout_vertical_margin"
android:layout_marginRight="@dimen/layout_vertical_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin"
android:minWidth="160dp"
android:text="@string/_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_incoming_request_description_2" />
<Button
android:id="@+id/sas_request_cancel_button"
style="@style/VectorButtonStyleFlat"
android:layout_gravity="end"
android:layout_marginEnd="@dimen/layout_vertical_margin"
android:layout_marginRight="@dimen/layout_vertical_margin"
android:text="@string/cancel"
app:layout_constraintEnd_toStartOf="@+id/sas_request_continue_button"
app:layout_constraintTop_toTopOf="@+id/sas_request_continue_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/sas_verification_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/sas_verify_title"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/sas_verification_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/sas_security_advise"
android:textColor="?android:attr/textColorSecondary" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal">
<Button
android:id="@+id/sas_cancel_button"
style="@style/VectorButtonStyleFlat"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/cancel" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin">
<Button
android:id="@+id/sas_start_button"
style="@style/VectorButtonStyle"
android:minWidth="160dp"
android:text="@string/sas_verify_start_button_title" />
<ProgressBar
android:id="@+id/sas_start_button_loading"
android:layout_width="19dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>
<Button
android:id="@+id/sas_legacy_verification"
style="@style/VectorButtonStyleFlat"
android:layout_gravity="end"
android:layout_margin="@dimen/layout_horizontal_margin"
android:text="@string/sas_legacy_verification_button_title"
android:visibility="visible" />
<TextView
android:id="@+id/sas_verifying_keys"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/sas_verifying_keys"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/sas_verification_verified_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text="@string/sas_verified"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/sas_verification_verified_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:gravity="center"
android:text="@string/sas_verified_successful"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_verification_verified_title" />
<TextView
android:id="@+id/sas_verification_verified_description_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_vertical_margin"
android:gravity="center"
android:text="@string/sas_verified_successful_description"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sas_verification_verified_description" />
<Button
android:id="@+id/sas_verification_verified_done_button"
style="@style/VectorButtonStyle"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:layout_marginEnd="@dimen/layout_vertical_margin"
android:layout_marginRight="@dimen/layout_vertical_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin"
android:minWidth="160dp"
android:text="@string/sas_got_it"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sas_verification_verified_description_2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:gravity="center"
android:orientation="vertical">
<!-- size in dp, because we do not want the display to be impacted by font size setting -->
<TextView
android:id="@+id/item_emoji_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="40dp"
tools:ignore="SpUsage"
tools:text="🌵" />
<TextView
android:id="@+id/item_emoji_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="4dp"
tools:text="@string/verification_emoji_cactus" />
</LinearLayout>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"
android:minHeight="50dp">
<TextView
android:id="@+id/item_generic_title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/item_generic_description_text"
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Item Title"
tools:textSize="14sp" />
<TextView
android:id="@+id/item_generic_description_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/item_generic_action_button"
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/item_generic_title_text"
tools:text="At totam delectus et aliquid dolorem. Consectetur voluptas tempore et non blanditiis id optio. Dolorum impedit quidem minus nihil. "
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/item_generic_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="item_generic_accessory_image,item_generic_progress_bar"
tools:ignore="MissingConstraints" />
<ImageView
android:id="@+id/item_generic_accessory_image"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/item_generic_description_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/item_generic_title_text"
tools:srcCompat="@drawable/e2e_warning"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/item_generic_progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/item_generic_description_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/item_generic_title_text"
tools:visibility="visible" />
<Button
android:id="@+id/item_generic_action_button"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:background="?attr/colorAccent"
android:padding="8dp"
android:textColor="@android:color/white"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/item_generic_description_text"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/item_generic_description_text"
tools:text="@string/settings_troubleshoot_test_device_settings_quickfix"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="vertical">
<Button
android:id="@+id/keys_backup_settings_footer_button1"
style="@style/VectorButtonStyle"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:minWidth="200dp"
android:visibility="gone"
tools:text="@string/keys_backup_settings_restore_backup_button"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/keys_backup_settings_footer_button2"
style="@style/VectorButtonStyle"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:minWidth="200dp"
android:visibility="gone"
app:backgroundTint="@color/vector_warning_color"
tools:text="@string/keys_backup_settings_delete_backup_button"
tools:visibility="visible" />
</LinearLayout>

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?vctr_list_header_background_color"
android:minHeight="67dp">
<ImageView
android:id="@+id/view_keys_backup_banner_picto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="19dp"
android:layout_marginLeft="19dp"
android:src="@drawable/key_small"
android:tint="?android:textColorTertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/view_keys_backup_banner_text_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginLeft="27dp"
android:text="@string/keys_backup_banner_setup_line1"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@id/view_keys_backup_banner_text_2"
app:layout_constraintEnd_toStartOf="@id/view_keys_backup_banner_barrier"
app:layout_constraintStart_toEndOf="@id/view_keys_backup_banner_picto"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/view_keys_backup_banner_text_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginLeft="27dp"
android:text="@string/keys_backup_banner_setup_line2"
android:textColor="?colorAccent"
android:textSize="15sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/view_keys_backup_banner_barrier"
app:layout_constraintStart_toEndOf="@id/view_keys_backup_banner_picto"
app:layout_constraintTop_toBottomOf="@id/view_keys_backup_banner_text_1"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/view_keys_backup_banner_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="view_keys_backup_banner_close,view_keys_backup_banner_loading"
tools:ignore="MissingConstraints" />
<ProgressBar
android:id="@+id/view_keys_backup_banner_loading"
style="?android:attr/progressBarStyle"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="14dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<!-- Trick to align the close picto to top of text -->
<androidx.constraintlayout.widget.Group
android:id="@+id/view_keys_backup_banner_close_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="view_keys_backup_banner_close,view_keys_backup_banner_close_image"
tools:visibility="visible" />
<View
android:id="@+id/view_keys_backup_banner_close"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/view_keys_backup_banner_close_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:src="@drawable/ic_small_close"
app:layout_constraintEnd_toEndOf="@id/view_keys_backup_banner_close"
app:layout_constraintStart_toStartOf="@id/view_keys_backup_banner_close"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?vctr_line_divider"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:orientation="horizontal"
tools:parentTag="android.widget.LinearLayout">
<View
android:id="@+id/password_strength_bar_1"
android:layout_width="0dp"
android:layout_height="4dp"
android:layout_marginEnd="5dp"
android:layout_marginRight="5dp"
android:layout_weight="1"
tools:background="@color/password_strength_bar_weak" />
<View
android:id="@+id/password_strength_bar_2"
android:layout_width="0dp"
android:layout_height="4dp"
android:layout_marginEnd="5dp"
android:layout_marginRight="5dp"
android:layout_weight="1"
tools:background="@color/password_strength_bar_low" />
<View
android:id="@+id/password_strength_bar_3"
android:layout_width="0dp"
android:layout_height="4dp"
android:layout_marginEnd="5dp"
android:layout_marginRight="5dp"
android:layout_weight="1"
tools:background="@color/password_strength_bar_ok" />
<View
android:id="@+id/password_strength_bar_4"
android:layout_width="0dp"
android:layout_height="4dp"
android:layout_weight="1"
tools:background="@color/password_strength_bar_strong" />
</merge>

View File

@ -41,8 +41,8 @@
<string name="room_no_conference_call_in_encrypted_rooms">مكالمات الاجتماعات ليست مدعومة في الغرفة المعمّاة</string>
<string name="send_anyway">أرسِل بأي حال</string>
<string name="action_sign_out">اخرج</string>
<string name="action_voice_call">مكالمة صورية</string>
<string name="action_video_call">مكالمة صوتية</string>
<string name="action_voice_call">مكالمة صوتية</string>
<string name="action_video_call">مكالمة صورية</string>
<string name="action_global_search">بحث عمومي</string>
<string name="action_mark_all_as_read">علّمها كلّها كمقروءة</string>
<string name="action_open">افتح</string>

View File

@ -10,7 +10,7 @@
<string name="dark_theme">Тъмна тема</string>
<string name="black_them">Черна тема</string>
<string name="notification_sync_in_progress">Синхронизиране</string>
<string name="notification_sync_in_progress">Синхронизиране</string>
<string name="title_activity_home">Съобщения</string>
<string name="title_activity_room">Стая</string>
<string name="title_activity_settings">Настройки</string>
@ -1358,8 +1358,8 @@
<string name="autodiscover_invalid_response">Невалиден отговор при опит за откриване на адреса на сървъра</string>
<string name="autodiscover_well_known_autofill_dialog_title">Опции за откриване на сървър</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot откри конфигурация за собствен сървър за домейна от потребителското Ви име \"%s\":
\n%s</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot откри конфигурация за собствен сървър за домейна от потребителското Ви име \"%1$s\":
\n%2$s</string>
<string name="autodiscover_well_known_autofill_confirm">Използвай конфигурацията</string>
<string name="notification_sync_init">Инициализиране на услугата</string>
@ -1388,4 +1388,6 @@
<string name="notification_sender_me">Аз</string>
<string name="notification_inline_reply_failed">** Неуспешно изпращане - моля, отворете стаята</string>
<string name="error_jitsi_not_supported_on_old_device">Извиняваме се, но конферентни разговори с Jitsi не се поддържат на стари устройства (устройства с Android OS под 5.0)</string>
</resources>

View File

@ -8,7 +8,7 @@
<string name="black_them">কালো থিম</string>
<string name="status_theme">Status.im থিম</string>
<string name="notification_sync_in_progress">সিংক্রোনাইজ হচ্ছে</string>
<string name="notification_sync_in_progress">সিংক্রোনাইজ হচ্ছে</string>
<string name="notification_listening_for_events">ইভেন্টের জন্য শোনা হচ্ছে</string>
<string name="notification_noisy_notifications">সশব্দ বিজ্ঞপ্তিগুলি</string>
<string name="notification_silent_notifications">নীরব বিজ্ঞপ্তিগুলি</string>
@ -411,7 +411,7 @@
<string name="room_participants_create">সৃষ্টি</string>
<string name="room_participants_online">অনলাইন</string>
<string name="room_participants_offline">অফলিনে</string>
<string name="room_participants_offline">অফলাইন</string>
<string name="room_participants_idle">অলস</string>
<string name="room_participants_now">এখুন %1$s</string>
<string name="room_participants_ago">%1$s %2$s পূর্বে</string>
@ -499,14 +499,14 @@
<string name="room_details_title">ঘরের বিস্তারিত</string>
<string name="room_details_people">লোকজন</string>
<string name="room_details_files">নথিগুলি</string>
<string name="room_details_settings">নির্ধারণ</string>
<string name="room_details_settings">সেটিংস</string>
<plurals name="room_details_selected">
<item quantity="one">%d নির্বাচিত</item>
<item quantity="other" />
</plurals>
<string name="malformed_id">বিকৃত পরিচয়।একটি ইমেইল ঠিকানা বা একটি মাধ্যমিক পরিচয় হতে হবে যেমন \'@localpart:domain\'</string>
<string name="room_details_people_invited_group_name">আমন্ত্রিত</string>
<string name="room_details_people_present_group_name">যোগকর</string>
<string name="room_details_people_present_group_name">যোগ করেছেন</string>
<string name="room_event_action_report_prompt_reason">বিষয়বস্তুর বিবরণী দেয়ার জন কারণ</string>
<string name="room_event_action_report_prompt_ignore_user">ব্যাবহারকারির কাছ থেকে আপনি কি সব বার্তা লোকাতে চান\?উল্লেখ্য এই প্রতিক্রিয়াটি চালু করবেন এবং এটি কিছু সময় নিতে পারে।</string>
@ -556,7 +556,7 @@
<string name="room_settings_add_homescreen_shortcut">ঘরের পর্দার ছোটোখাটো যোগ করুন</string>
<string name="room_sliding_menu_messages">বার্তা</string>
<string name="room_sliding_menu_settings">নির্ধারণ</string>
<string name="room_sliding_menu_settings">সেটিংস</string>
<string name="room_sliding_menu_version">সংস্করণ</string>
<string name="room_sliding_menu_version_x">সংস্করণ %s</string>
<string name="room_sliding_menu_term_and_conditions">শর্তাবলী</string>
@ -583,20 +583,21 @@
<string name="settings_troubleshoot_diagnostic">ডায়াগনস্টিকস এর সমস্যা সমাধান</string>
<string name="settings_troubleshoot_diagnostic_run_button_title">পরীক্ষাগুলি চালাও</string>
<string name="settings_troubleshoot_diagnostic_running_status">"(%1$d of %2$d) চলছে… "</string>
<string name="settings_troubleshoot_test_account_settings_failed">বিজ্ঞপ্তি আপনার অ্যাকাউন্টের জন্য নিষ্ক্রিয় করা হয়েছে।</string>
<string name="settings_troubleshoot_test_account_settings_failed">বিজ্ঞপ্তি আপনার অ্যাকাউন্টের জন্য নিষ্ক্রিয় করা হয়েছে।
\nঅনুগ্রহ করে একাউন্ট সেটিংস যাচাই করে নিন।</string>
<string name="settings_troubleshoot_test_account_settings_quickfix">সক্ষম</string>
<string name="settings_troubleshoot_test_device_settings_title">যন্ত্র সেটিংস।</string>
<string name="settings_troubleshoot_test_device_settings_success">বিজ্ঞপ্তি এই ডিভাইসের জন্য সক্রিয় করা হয়েছে।</string>
<string name="settings_troubleshoot_test_device_settings_failed">বিজ্ঞপ্তি এই ডিভাইসের জন্য অনুমতি দেওয়া হয় নি।
\nRiot এর সেটিংস চেক করুন।</string>
<string name="settings_troubleshoot_test_device_settings_failed">বিজ্ঞপ্তি এই ডিভাইসের জন্য অনুমতি দেওয়া হয় নি।
\nRiot এর সেটিংস যাচাই করুন।</string>
<string name="settings_troubleshoot_test_device_settings_quickfix">সক্ষম</string>
<string name="settings_troubleshoot_test_bing_settings_title">কাস্টম সেটিংস।</string>
<string name="settings_troubleshoot_test_bing_settings_success_with_warn">লক্ষ্য করুন যে কিছু বার্তা টাইপ নীরব করা হয়েছে (কোন শব্দ ছাড়াই একটি বিজ্ঞপ্তি তৈরি করবে)।</string>
<string name="settings_troubleshoot_test_bing_settings_failed">কিছু বিজ্ঞপ্তি আপনার কাস্টম সেটিংস এ নিষ্ক্রিয় করা হয়েছে।</string>
<string name="settings_troubleshoot_test_bing_settings_failed_to_load_rules">কাস্টম নিয়ম লোড করতে ব্যর্থ হয়েছে, আবার চেষ্টা করুন।</string>
<string name="settings_troubleshoot_test_bing_settings_quickfix">সেটিংস চেক করুন</string>
<string name="settings_troubleshoot_test_bing_settings_quickfix">সেটিংস যাচাই করুন</string>
<string name="settings_troubleshoot_test_play_services_title">Play Services পরীক্ষা</string>
<string name="settings_troubleshoot_test_play_services_success">গুগল প্লে সার্ভিসেস APK পাওয়া গেছে এবং আপ টু ডেট রয়েছে।</string>
@ -611,8 +612,8 @@
\n%1$s</string>
<string name="settings_troubleshoot_test_fcm_failed_too_many_registration">[%1$s]
\nএই ত্রুটিটি Riot এর নিয়ন্ত্রণের বাইরে এবং Google এর মতে, এই ত্রুটিটি ইঙ্গিত করে যে ডিভাইসটিতে FCM এর সাথে নিবন্ধিত অনেকগুলি অ্যাপ্লিকেশন রয়েছে। ত্রুটিগুলি কেবলমাত্র অ্যাপ্লিকেশনের চরম সংখ্যাগুলিতে ঘটে থাকে, তাই এটি গড় ব্যবহারকারীকে প্রভাবিত করবে না।</string>
<string name="settings_troubleshoot_test_fcm_failed_service_not_available">[%1$s]
\nএই ত্রুটি Riot এর নিয়ন্ত্রণ বাইরে। এটা বিভিন্ন কারণে ঘটতে পারে। আপনি পরে পুনরায় চেষ্টা করলে হয়তো এটি কাজ করবে, আপনি এটিও পরীক্ষা করতে পারেন যে Google Play পরিষেবাটি সিস্টেম সেটিংসে ডেটা ব্যবহারের ক্ষেত্রে সীমাবদ্ধ নয়, অথবা আপনার ডিভাইসের ঘড়ি সঠিক, বা এটি কাস্টম রমতে ঘটতে পারে।</string>
<string name="settings_troubleshoot_test_fcm_failed_service_not_available">[%1$s]
\nএই ত্রুটি Riot এর নিন্ত্রণের বাইরে। এটা বিভিন্ন কারণে ঘটতে পারে। আপনি পরে পুনরায় চেষ্টা করলে হয়তো এটি কাজ করবে, আপনি এটিও পরীক্ষা করতে পারেন যে Google Play পরিষেবাটি সিস্টেম সেটিংসে ডেটা ব্যবহারের ক্ষেত্রে সীমাবদ্ধ নয়, অথবা আপনার ডিভাইসের ঘড়ি সঠিক, বা এটি কাস্টম রমতে ঘটতে পারে।</string>
<string name="settings_troubleshoot_test_fcm_failed_account_missing">[%1$s]
\nএই ত্রুটি Riot এর নিয়ন্ত্রণের বাইরে। ফোনে কোন গুগল একাউন্ট নেই। অ্যাকাউন্ট ম্যানেজার খুলুন এবং একটি গুগল একাউন্ট যোগ করুন।</string>
<string name="settings_troubleshoot_test_fcm_failed_account_missing_quick_fix">একাউন্ট যোগ করুন</string>
@ -657,13 +658,49 @@
<string name="settings_troubleshoot_diagnostic_failure_status_with_quickfix">এক অথবা অধিক পরীক্ষা বার্থ হয়েছে,প্রস্তাবিত ঠিক করে চেষ্টা করুন(es).</string>
<string name="settings_troubleshoot_diagnostic_failure_status_no_quickfix">এক অথবা অধিক পরীক্ষা ব্যর্থ হয়েছে,দয়া করে জমা করুন একটা গুরুত্বপূর্ণ খসড়া যেটা সাহায্য করবে অনুসন্ধান করতে।</string>
<string name="settings_troubleshoot_test_system_settings_title">পদ্ধতি নির্ধারণ।</string>
<string name="settings_troubleshoot_test_system_settings_success">বিজ্ঞাপ্তিকে পদ্ধতি নির্ধারণ এর মাধ্যমে সক্রিয় করা হয়েছে।</string>
<string name="settings_troubleshoot_test_system_settings_failed">বিজ্ঞাপ্তিকে পদ্ধতি নির্ধারণ এর মাধ্যমে নিষ্ক্রিয় করা হয়েছে।
\nদেয়া করে দেখে নিন পদ্ধতি নির্ধারণ।</string>
<string name="open_settings">খুলুন নির্ধারণটি</string>
<string name="settings_troubleshoot_test_system_settings_title">সিস্টেমের সেটিংস।</string>
<string name="settings_troubleshoot_test_system_settings_success">বিজ্ঞাপ্তিকে সিস্টেমের সেটিংস এর মাধ্যমে সক্রিয় করা হয়েছে।</string>
<string name="settings_troubleshoot_test_system_settings_failed">বিজ্ঞাপ্তিকে সিস্টেমের সেটিংস এর মাধ্যমে নিষ্ক্রিয় করা হয়েছে।
\nদেয়া করে সিস্টেমের সেটিংসগুলি যাচাই করে নিন।</string>
<string name="open_settings">সেটিংস খুলুন</string>
<string name="settings_troubleshoot_test_account_settings_title">গণনা নির্ধারণ।</string>
<string name="settings_troubleshoot_test_account_settings_title">অক্কোউন্টের সেটিংস।</string>
<string name="settings_troubleshoot_test_account_settings_success">বিজ্ঞাপ্তি আপনার একাউন্টএর জন্য সক্রিয় করা হোক.</string>
<string name="action_mark_room_read">পঠিত হিসেবে চিহ্নিত</string>
<string name="settings_user_settings">ব্যবহারকারী সেটিংস</string>
<string name="settings_inline_url_preview">ইনলাইন URL পূর্বরূপ</string>
<string name="settings_inline_url_preview_summary">বার্তাগুলি মধ্যে থাকা লিংকগুলি প্রিভিউ করে যখন আপনার হোম সার্ভার এই বৈশিষ্ট্য টি সাপোর্ট করে।</string>
<string name="settings_send_typing_notifs">টাইপিং বিজ্ঞপ্তি পাঠান</string>
<string name="settings_preview_media_before_sending">পাঠানোর আগে মিডিয়া প্রিভিউ কর</string>
<string name="settings_interface_language">ভাষা</string>
<string name="settings_select_language">ভাষা বেছে নিন</string>
<string name="settings_theme">থিম</string>
<string name="encryption_information_verify_device_warning">এই ডিভাইসটি বিশ্বাসযোগ্য হতে পারে তা যাচাই করতে, অন্য কোন উপায়ে (যেমন ব্যক্তি বা ফোন কল) ব্যবহার করে তার মালিকের সাথে যোগাযোগ করুন এবং এই ডিভাইসটির জন্য তাদের ব্যবহারকারী সেটিংসে এ কুঞ্জি দেখছে তা তাদের জিজ্ঞাসা করুন নীচের কুঞ্জিটির সাথে মেলে কিনা:</string>
<string name="e2e_enabling_on_app_update">দাঙ্গা এখন শেষ-থেকে-শেষ এনক্রিপশন সমর্থন করে তবে এটি সক্ষম করতে আপনাকে আবার লগ ইন করতে হবে।
\n
\nআপনি এখন বা পরে এপ্লিকেশন সেটিংস থেকে এটি করতে পারেন।</string>
<string name="font_size">অক্ষর এর আকার</string>
<string name="you_added_a_new_device">আপনি একটি নতুন ডিভাইস \'%s\' যোগ করেছেন, যা এনক্রিপশন কীগুলির জন্য অনুরোধ করছে।</string>
<string name="command_description_clear_scalar_token">ম্যাট্রিক্স অ্যাপ্লিকেশন ব্যবস্থাপনা ঠিক করতে</string>
<string name="encrypted_message">এনক্রিপ্ট করা বার্তা</string>
<string name="joined">যোগ করেছেন</string>
<string name="has_been_kicked">আপনি %1$s থেকে %2$s দ্বারা লাথি খেয়েছেন</string>
<string name="merged_events_expand">সম্প্রসারিত</string>
<string name="merged_events_collapse">বন্ধ</string>
<string name="keys_backup_restore_with_key_helper">আপনার পুনরুদ্ধারের কুঞ্জি হারিয়ে গেছে\? আপনি সেটিংস একটি নতুন সেট আপ করতে পারেন।</string>
<string name="keys_backup_restore_success_description">"%1$d সেশন কী পুনরুদ্ধার করা হয়েছে, এবং %2$d নতুন কী (গুলি) জোড়া হয়েছে যা এই ডিভাইসটি জানত না"</string>
<plurals name="keys_backup_restore_success_description_part2">
<item quantity="one">%d টি নতুন কী এই ডিভাইসে যোগ করা হয়েছে।</item>
<item quantity="other">%d টি নতুন কী এই ডিভাইসে যোগ করা হয়েছে।</item>
</plurals>
<string name="new_recovery_method_popup_description">একটি নতুন সুরক্ষিত বার্তা কুঞ্জি ব্যাকআপ সনাক্ত করা হয়েছে।
\n
\nআপনি যদি নতুন পুনরুদ্ধারের পদ্ধতি সেট না করে থাকেন তবে একজন আক্রমণকারী আপনার অ্যাকাউন্ট অ্যাক্সেস করার চেষ্টা করছেন। আপনার অ্যাকাউন্ট পাসওয়ার্ড পরিবর্তন করুন এবং সেটিংসে অবিলম্বে একটি নতুন পুনরুদ্ধার পদ্ধতি সেট করুন।</string>
</resources>

View File

@ -1197,7 +1197,7 @@ Versuche die Anwendung neuzustarten.</string>
<string name="no_valid_google_play_services_apk">Keine validen Google-Play-Dienste gefunden. Benachrichtigungen könnten nicht richtig funktionieren.</string>
<string name="store_title">Riot.im - Kommunizierte auf deine Weise</string>
<string name="store_title">Riot.im - Kommuniziere auf deine Weise</string>
<string name="store_short_description">Eine universelle, sichere Chat-App - komplett unter deiner Kontrolle.</string>
<string name="store_full_description">"Eine Chat-App unter deiner Kontrolle und total flexibel. Riot lässt dich auf die Art kommunizieren wie du willst. Die App wurde gemacht für [matrix] - dem Standard für offene, dezentrale Komunikation.
@ -1423,8 +1423,8 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A
<string name="autodiscover_invalid_response">Ungültige Antwort beim Entdecken des Heimservers</string>
<string name="autodiscover_well_known_autofill_dialog_title">Serveroptionen vervollständigen</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot hat eine benutzerdefinierte Serverkonfiguration für die Domäne deines Benutzernamens gefunden \"%s\":
\n%s</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot hat eine benutzerdefinierte Serverkonfiguration für die Domäne deines Benutzernamens gefunden \"%1$s\":
\n%2$s</string>
<string name="autodiscover_well_known_autofill_confirm">Nutze Konfiguration</string>
<string name="notification_sync_init">Initialisiere Dienst</string>

View File

@ -663,7 +663,7 @@ Gailu ezezagunak:</string>
<string name="dark_theme">Itxura iluna</string>
<string name="black_them">Itxura beltza</string>
<string name="notification_sync_in_progress">Sinkronizatzen</string>
<string name="notification_sync_in_progress">Sinkronizatzen</string>
<string name="notification_listening_for_events">Entzun gertaerak</string>
<string name="login_mobile_device">Mugikorra</string>
@ -1376,8 +1376,8 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.</string>
<string name="autodiscover_invalid_response">Baliogabeko hasiera-zerbitzari deskubritze erantzuna</string>
<string name="autodiscover_well_known_autofill_dialog_title">Automatikoki osatu zerbitzariaren aukerak</string>
<string name="autodiscover_well_known_autofill_dialog_message">"Riot-ek pertsonalizatutako zerbitzari konfigurazio bat antzeman du zure erabiltzaile id-arentzat \"%s\" domeinuan:
\n%s"</string>
<string name="autodiscover_well_known_autofill_dialog_message">"Riot-ek pertsonalizatutako zerbitzari konfigurazio bat antzeman du zure erabiltzaile id-arentzat \"%1$s\" domeinuan:
\n%2$s"</string>
<string name="autodiscover_well_known_autofill_confirm">Erabili konfigurazioa</string>
<string name="action_mark_room_read">Markatu irakurritako gisa</string>
@ -1398,4 +1398,6 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.</string>
<string name="notification_sender_me">Ni</string>
<string name="notification_inline_reply_failed">** Bidalketak huts egin du, ireki gela</string>
<string name="error_jitsi_not_supported_on_old_device">Sentitzen dugu, gailu zaharretan ezin dira Jitsi bidezko konferentzia deiak egin (Android OS 5.0 baino zaharragoak)</string>
</resources>

View File

@ -24,14 +24,14 @@
<string name="resend">ارسال مجدد</string>
<string name="quote">نقل قول</string>
<string name="share">اشتراک گذاری</string>
<string name="later">بعدا</string>
<string name="later">بعداً</string>
<string name="view_source">مشاهده منبع</string>
<string name="delete">حذف</string>
<string name="rename">تغییر نام</string>
<string name="report_content">گزارش محتوا</string>
<string name="active_call">تماس فعال</string>
<string name="ongoing_conference_call_voice">صدا</string>
<string name="ongoing_conference_call_video">ویدیو</string>
<string name="ongoing_conference_call_voice">صوتی</string>
<string name="ongoing_conference_call_video">تصویری</string>
<string name="device_information">اطلاعات دستگاه</string>
<string name="send_anyway">به هر حال ارسال کنید</string>
<string name="or">یا</string>
@ -56,7 +56,7 @@
<string name="bottom_action_rooms">اتاق‌ها</string>
<string name="low_priority_header">اولویت کم</string>
<string name="direct_chats_header">گفتگو</string>
<string name="direct_chats_header">گفتگوها</string>
<string name="no_conversation_placeholder">مکالمه‌ای نیست</string>
<string name="rooms_header">اتاق‌ها</string>
<plurals name="public_room_nb_users">
@ -70,11 +70,11 @@
<string name="send_bug_report">گزارش اشکال</string>
<string name="read_receipt">خواندن</string>
<string name="join_room">عضویت در اتاق</string>
<string name="join_room">پیوستن به اتاق</string>
<string name="username">نام کاربری</string>
<string name="create_account">ثبت‌نام</string>
<string name="create_account">ساخت حساب</string>
<string name="login">ورود</string>
<string name="logout">خروج</string>
<string name="logout">خروج از حساب</string>
<string name="search">جستجو</string>
<string name="start_new_chat">شروع گپ جدید</string>
@ -82,17 +82,17 @@
<string name="start_video_call">شروع تماس تصویری</string>
<string name="option_take_photo_video">عکس یا فیلم بگیرید</string>
<string name="option_take_photo">عکس گرفتن</string>
<string name="option_take_photo">عکس بگیرید</string>
<string name="option_take_video">فیلم بگیرید</string>
<string name="auth_login">ورود</string>
<string name="auth_register">ثبت نام</string>
<string name="auth_register">ساخت حساب</string>
<string name="auth_submit">ارسال</string>
<string name="auth_skip">رد کردن</string>
<string name="auth_send_reset_email">ارسال ایمیل مجدد</string>
<string name="auth_skip">رد شدن</string>
<string name="auth_send_reset_email">ارسال ایمیل بازیابی</string>
<string name="auth_user_id_placeholder">ایمیل یا نام کاربری</string>
<string name="auth_password_placeholder">رمز عبور</string>
<string name="auth_new_password_placeholder">رمز عبور جدید</string>
<string name="auth_password_placeholder">گذرواژه</string>
<string name="auth_new_password_placeholder">گذرواژه جدید</string>
<string name="auth_user_name_placeholder">نام کاربری</string>
<string name="auth_email_placeholder">آدرس ایمیل</string>
<string name="auth_opt_email_placeholder">آدرس ایمیل (اختیاری)</string>
@ -102,9 +102,9 @@
<string name="auth_repeat_new_password_placeholder">رمز عبور جدید خود را تأیید کنید</string>
<string name="auth_invalid_login_param">نام کاربری و/یا رمز عبور نامعتبر می‌باشد</string>
<string name="auth_invalid_password">رمز عبور خیلی کوتاه است (حداقل ۶)</string>
<string name="auth_invalid_email">آدرس ایمیل به نظر می‌رسد معتبر نباشد</string>
<string name="auth_invalid_phone">شماره تلفن به نظر می‌رسد معتبر نباشد</string>
<string name="auth_password_dont_match">رمز عبورها مطابقت ندارد</string>
<string name="auth_invalid_email">آدرس ایمیل نامعتبر به نظر می‌رسد</string>
<string name="auth_invalid_phone">شماره تلفن نامعتبر به نظر می‌رسد</string>
<string name="auth_password_dont_match">گذرواژه‌ها مطابقت ندارد</string>
<string name="auth_forgot_password">رمز عبور را فراموش کردید؟</string>
<string name="auth_use_server_options">از گزینه‌های سرور سفارشی استفاده کنید (پیشرفته)</string>
<string name="auth_email_validation_message">لطفا ایمیل خود را برای ادامه ثبت نام بررسی کنید</string>
@ -116,7 +116,7 @@
<string name="login_error_user_in_use">این نام کاربری قبلا استفاده شده است</string>
<string name="groups_list">فهرست گروه‌ها</string>
<string name="compression_options">"ارسال به عنوان "</string>
<string name="compression_options">ارسال به عنوان</string>
<string name="compression_opt_list_original">اصلی</string>
<string name="compression_opt_list_large">بزرگ</string>
<string name="compression_opt_list_medium">متوسط</string>
@ -129,8 +129,8 @@
<string name="today">امروز</string>
<string name="room_info_room_name">نام اتاق</string>
<string name="notification_sync_in_progress">همگام‌سازی</string>
<string name="call_anyway">به هر حال تماس بگیر</string>
<string name="notification_sync_in_progress">همگام‌سازی</string>
<string name="call_anyway">به هر حال تماس بگیرید</string>
<string name="missing_permissions_title_to_start_conf_call">نمی‌توان تماس را شروع کرد</string>
<string name="cannot_start_call">نمی‌توان تماس را شروع کرد، لطفاً بعداً تلاش نمایید</string>
<string name="accept">پذیرفتن</string>
@ -144,7 +144,7 @@
<string name="bottom_action_groups">جامعه‌ها</string>
<string name="home_filter_placeholder_home">جستجوی اتاق‌ها</string>
<string name="home_filter_placeholder_favorites">جستجوی پسندیده‌ها</string>
<string name="home_filter_placeholder_favorites">جستجوی پسندها</string>
<string name="home_filter_placeholder_people">جستجوی افراد</string>
<string name="home_filter_placeholder_rooms">جستجوی اتاق‌ها</string>
<string name="home_filter_placeholder_groups">جستجوی جامعه‌ها</string>
@ -155,7 +155,7 @@
<string name="local_address_book_header">مخاطبین من</string>
<string name="user_directory_header">فهرست کاربران</string>
<string name="matrix_only_filter">فقط نمایش مخاطبین ماتریکس</string>
<string name="no_contact_access_placeholder">شما اجازه‌ی دسترسی به مخاطبین را به Riot نداده‌اید</string>
<string name="no_contact_access_placeholder">شما اجازه‌ی دسترسی به مخاطبین را به برنامه نداده‌اید</string>
<string name="no_result_placeholder">نتیجه‌ای نیست</string>
<string name="rooms_directory_header">فهرست اتاق‌ها</string>
@ -189,8 +189,7 @@
<string name="option_send_files">ارسال فایل‌ها</string>
<string name="option_send_sticker">ارسال استیکر</string>
<string name="no_sticker_application_dialog_content">شما بسته‌ی استیکر فعالی ندارید.
تمایل دارید بسته‌ی استیکر جدید اضافه کنید؟</string>
\nتمایل دارید بسته‌ی استیکر جدیدی اضافه کنید؟</string>
<string name="go_on_with">ادامه با…</string>
<string name="error_no_external_application_found">متاسفانه برنامه‌ای روی گوشی شما برای انجام این کار پیدا نشد.</string>
@ -223,4 +222,78 @@
<string name="reject">رد کردن</string>
<string name="list_members">اعضای لیست</string>
<string name="call_connected">تماس برقرار شد</string>
<string name="call_connecting">برقراری تماس…</string>
<string name="call_ended">تماس پایان یافت</string>
<string name="call_ring">شماره‌گیری…</string>
<string name="incoming_call">تماس ورودی</string>
<string name="incoming_video_call">تماس ویدئویی ورودی</string>
<string name="incoming_voice_call">تماس صوتی ورودی</string>
<string name="call_in_progress">تماس در جریان است…</string>
<string name="video_call_in_progress">تماس ویدئویی در جریان است…</string>
<string name="call_error_user_not_responding">طرف مقابل پاسخ نداد.</string>
<string name="call_error_ice_failed">برقراری ارتباط چندرسانه‌ای ممکن نشد</string>
<string name="call_error_camera_init_failed">راه‌اندازی دوربین ممکن نبود</string>
<string name="call_error_answered_elsewhere">پاسخ تماس از کاربری نامعتبر دریافت شد</string>
<plurals name="room_title_members">
<item quantity="one">%d عضو</item>
<item quantity="other">%d عضو</item>
</plurals>
<string name="room_title_one_member">۱ عضو</string>
<plurals name="format_time_s">
<item quantity="one">%d ثانیه</item>
<item quantity="other">%d ثانیه</item>
</plurals>
<plurals name="format_time_m">
<item quantity="one">%d دقیقه</item>
<item quantity="other">%d دقیقه</item>
</plurals>
<plurals name="format_time_h">
<item quantity="one">%d ساعت</item>
<item quantity="other">%d ساعت</item>
</plurals>
<plurals name="format_time_d">
<item quantity="one">%d روز</item>
<item quantity="other">%d روز</item>
</plurals>
<string name="room_participants_leave_prompt_title">ترک اتاق</string>
<string name="room_participants_leave_prompt_msg">آیا از ترک این اتاق اطمینان دارید؟</string>
<string name="room_participants_remove_prompt_msg">آیا می‌خواهید %s را از این گفتگو حذف کنید؟</string>
<string name="room_participants_create">ساخت</string>
<string name="room_participants_online">آنلاین</string>
<string name="room_participants_offline">آفلاین</string>
<string name="room_participants_idle">بیکار</string>
<string name="room_participants_now">در حال حاضر %1$s</string>
<string name="room_participants_ago">%1$s در %2$s پیش</string>
<string name="room_participants_header_admin_tools">ابزارهای مدیر</string>
<string name="room_participants_header_call">تماس</string>
<string name="room_participants_header_direct_chats">پیام‌های شخصی</string>
<string name="room_participants_header_devices">دستگاه‌ها</string>
<string name="room_participants_action_invite">دعوت</string>
<string name="room_participants_action_leave">ترک این اتاق</string>
<string name="room_participants_action_remove">حذف از این اتاق</string>
<string name="stay">ماندن</string>
<string name="redact">حذف</string>
<string name="download">دریافت</string>
<string name="ongoing_conference_call">کنفرانس در حال برگزاری است.
\nبه صورت %1$s یا %2$s به آن بپیوندید</string>
<string name="missing_permissions_warning">به خاطر نداشتن مجوز دسترسی، برخی امکانات ممکن است در دسترس نباشند…</string>
<string name="missing_permissions_error">به خاطر نداشتن مجوز دسترسی، این اقدام ممکن نیست.</string>
<string name="missing_permissions_to_start_conf_call">برای آغاز کنفرانس نیاز به دسترسی دعوت اعضا دارید</string>
<string name="room_no_conference_call_in_encrypted_rooms">تماس گروهی در گفتگوهای رمز شده ممکن نیست</string>
<string name="skip">رد شدن</string>
<string name="done">انجام شد</string>
<string name="abort">انصراف</string>
<string name="ignore">نادیده‌گیری</string>
<string name="action_sign_out_confirmation_simple">آیا می‌خواهید از حساب کاربری خود خارج شوید؟</string>
<string name="action_mark_room_read">علامت‌گذاری به عنوان خوانده شده</string>
<string name="auth_login_sso">ورود با سامانه‌های احراز هویت مرکزی</string>
</resources>

View File

@ -348,7 +348,7 @@ Salli Riotin käyttää yhteystietojasi?</string>
<string name="room_two_users_are_typing">%1$s ja %2$s kirjoittavat…</string>
<string name="room_many_users_are_typing">%1$s, %2$s ja muut kirjoittavat…</string> <!-- ??? this ain't right -->
<string name="room_message_placeholder_encrypted">Lähetä salattu viesti…</string>
<string name="room_message_placeholder_not_encrypted">Lähetä viesti (ei salattu)…</string>
<string name="room_message_placeholder_not_encrypted">Lähetä viesti (salaamaton)…</string>
<string name="room_offline_notification">Yhteys palvelimeen katkesi.</string>
<string name="room_unsent_messages_notification">Viesteja ei lähetetty. %1$s tai %2$s?</string>
<string name="room_unknown_devices_messages_notification">Viestejä ei lähetetty koska huoneessa on tuntemattomia laitteita. %1$s tai %2$s?</string>
@ -597,9 +597,9 @@ Salli Riotin käyttää yhteystietojasi?</string>
<string name="room_settings_copy_room_address">Kopioi huoneen osoite</string>
<string name="room_settings_addresses_e2e_enabled">Tämä huone on salattu.</string>
<string name="room_settings_addresses_e2e_disabled">Tämä huone ei ole salattu.</string>
<string name="room_settings_addresses_e2e_encryption_warning">Käytä salausta
(HUOM! Salausta ei voi poistaa käytöstä!)</string>
<string name="room_settings_addresses_e2e_disabled">Tämä huone ei käytä salausta.</string>
<string name="room_settings_addresses_e2e_encryption_warning">Ota salaus käyttöön
\n(varoitus: salausta ei voi poistaa käytöstä!)</string>
<!-- Directory -->
<string name="directory_title">Luettelo</string>
@ -1409,8 +1409,8 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös
\nJos et asettanut uutta palautustapaa, hyökkääjä saattaa yrittää päästä käsiksi tunnukseesi. Vaihda tunnuksesi salasana ja aseta uusi palautustapa asetuksissa välittömästi.</string>
<string name="autodiscover_invalid_response">Epäkelpo kotipalvelimen löytövastaus</string>
<string name="autodiscover_well_known_autofill_dialog_title">Automaattitäydennyksen palvelinasetukset</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot löysi mukautetun palvelinasetuksen userId:si domainille ”%s”:
\n%s</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot löysi mukautetun palvelinasetuksen userId:si domainille ”%1$s”:
\n%2$s</string>
<string name="autodiscover_well_known_autofill_confirm">Käytä asetuksia</string>
<string name="notification_sync_init">Alustetaan palvelua</string>

View File

@ -1373,8 +1373,8 @@ Si vous navez pas configuré de nouvelle méthode de récupération, un attaq
<string name="autodiscover_invalid_response">Réponse de découverte du serveur daccueil non valide</string>
<string name="autodiscover_well_known_autofill_dialog_title">Auto-compléter les options du serveur</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot a détecté une configuration de serveur personnalisée pour le domaine de votre identifiant « %s » :
\n%s</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot a détecté une configuration de serveur personnalisée pour le domaine de votre identifiant « %1$s » :
\n%2$s</string>
<string name="autodiscover_well_known_autofill_confirm">Utiliser la configuration</string>
<string name="notification_sync_init">Initialisation du service</string>

View File

@ -1372,8 +1372,8 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
<string name="autodiscover_invalid_response">A Matrix szerver felderítésére érvénytelen válasz érkezett</string>
<string name="autodiscover_well_known_autofill_dialog_title">Szerver beállítások automatikus kiegészítése</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot egyedi szerver beállítást észlelt a felhasználói azonosítód domain-jéhez: \"%s\":
\n%s</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot egyedi szerver beállítást észlelt a felhasználói azonosítód domain-jéhez: \"%1$s\":
\n%2$s</string>
<string name="autodiscover_well_known_autofill_confirm">Beállítás használata</string>
<string name="notification_sync_init">Szolgáltatás indítása</string>

View File

@ -1417,8 +1417,8 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."</stri
<string name="autodiscover_invalid_response">Risposta alla scoperta di un homeserver non valida</string>
<string name="autodiscover_well_known_autofill_dialog_title">Opzioni autocompletamento server</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot ha rilevato una configurazione server personalizzata per il tuo dominio userId \"%s\":
\n%s</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot ha rilevato una configurazione server personalizzata per il tuo dominio userId \"%1$s\":
\n%2$s</string>
<string name="autodiscover_well_known_autofill_confirm">Usa configurazione</string>
<string name="notification_sync_init">Inizializzazione del servizio</string>

View File

@ -1450,8 +1450,8 @@
<string name="autodiscover_invalid_response">Ongeldig thuisserverontdekkingsantwoord</string>
<string name="autodiscover_well_known_autofill_dialog_title">Serveropties automatisch aanvullen</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot heeft een aangepaste serverconfiguratie gedetecteerd voor uw gebruikers-ID-domein %s:
\n%s</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot heeft een aangepaste serverconfiguratie gedetecteerd voor uw gebruikers-ID-domein %1$s:
\n%2$s</string>
<string name="autodiscover_well_known_autofill_confirm">Configuratie gebruiken</string>
<string name="error_jitsi_not_supported_on_old_device">Sorry, vergadergesprekken met Jitsi worden nog niet ondersteund op oudere apparaten (met een Android-versie lager dan 5.0)</string>

View File

@ -1331,8 +1331,8 @@ Që të garantoni se sju shpëton gjë, thjesht mbajeni të aktivizuar mekani
<string name="new_recovery_method_popup_was_me">Unë qeshë</string>
<string name="autodiscover_invalid_response">Përgjigje e pavlefshme zbulimi shërbyesi Home</string>
<string name="autodiscover_well_known_autofill_dialog_title">Mundësi Vetëplotësimi Shërbyesi</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot-i pikasi një formësim shërbyesi të përshtatur për përkatësinë tuaj userId \"%s\":
\n%s</string>
<string name="autodiscover_well_known_autofill_dialog_message">Riot-i pikasi një formësim shërbyesi të përshtatur për përkatësinë tuaj userId \"%1$s\":
\n%2$s</string>
<string name="autodiscover_well_known_autofill_confirm">Përdor Formësim</string>
<string name="notification_sync_init">Po gatitet shërbimi</string>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="number_of_emoji_per_line">7</integer>
</resources>

Some files were not shown because too many files have changed in this diff Show More